Mathew K Analytics

Lesson 8 · FastAPI deep dive

FastAPI Tutorial #8: Class-Based & Sub-Dependencies

Video eight of the eighteen-part series: dependencies that carry their own configuration. Callable classes as dependencies, Depends(Class) shorthand, bound…

⬇ Download notebookOpen in Colab ↗

📓 Full notebook

Download .ipynb

FastAPI Deep-Dive, Video 8: Class-based and Sub-Dependencies#

  • Video eight of the eighteen-part series: dependencies that carry their own configuration.
  • Callable classes as dependencies, Depends(Class) shorthand, bound methods, and sub-dependency chains.
  • Let's get into it.

Part 1: The Problem - Functions Can't Easily Carry Configuration#

from fastapi import FastAPI, Depends
from fastapi.testclient import TestClient
app = FastAPI()
def pagination(skip: int = 0, limit: int = 10):
    return {'skip': skip, 'limit': limit}
@app.get('/items')
def list_items(page: dict = Depends(pagination)):
    return page
client = TestClient(app)
print(client.get('/items?limit=500').json())
{'skip': 0, 'limit': 500}

Part 2: A Callable Class as a Dependency#

class Paginator:
    def __init__(self, max_limit: int = 100):
        self.max_limit = max_limit
    def __call__(self, skip: int = 0, limit: int = 10):
        return {'skip': skip, 'limit': min(limit, self.max_limit)}
strict_paginator = Paginator(max_limit=20)
@app.get('/items-v2')
def list_items_v2(page: dict = Depends(strict_paginator)):
    return page
print(client.get('/items-v2?limit=500').json())
{'skip': 0, 'limit': 20}

Part 3: Different Instances, Different Configuration#

loose_paginator = Paginator(max_limit=1000)
@app.get('/items-v3')
def list_items_v3(page: dict = Depends(loose_paginator)):
    return page
print(client.get('/items-v3?limit=500').json())
print(client.get('/items-v2?limit=500').json())
{'skip': 0, 'limit': 500}
{'skip': 0, 'limit': 20}

Part 4: Depends(Class) - Letting FastAPI Instantiate It#

class CommonParams:
    def __init__(self, q: str = '', skip: int = 0, limit: int = 10):
        self.q = q
        self.skip = skip
        self.limit = limit
@app.get('/search')
def search(params: CommonParams = Depends(CommonParams)):
    return {'q': params.q, 'skip': params.skip, 'limit': params.limit}
print(client.get('/search?q=mug&limit=5').json())
{'q': 'mug', 'skip': 0, 'limit': 5}

Part 5: The Shorthand - Depends() With No Argument#

@app.get('/search-v2')
def search_v2(params: CommonParams = Depends()):
    return {'q': params.q, 'skip': params.skip, 'limit': params.limit}
print(client.get('/search-v2?q=plate').json())
{'q': 'plate', 'skip': 0, 'limit': 10}

Part 6: A Bound Method as a Dependency#

class FeatureFlags:
    def __init__(self, enabled_features):
        self.enabled_features = enabled_features
    def check(self, feature: str = 'beta'):
        return {'feature': feature, 'enabled': feature in self.enabled_features}
flags = FeatureFlags(enabled_features={'beta', 'dark_mode'})
@app.get('/flags')
def flags_status(status: dict = Depends(flags.check)):
    return status
print(client.get('/flags?feature=dark_mode').json())
print(client.get('/flags?feature=missing').json())
{'feature': 'dark_mode', 'enabled': True}
{'feature': 'missing', 'enabled': False}

Part 7: A Class Dependency With Its Own Sub-Dependency#

def get_request_id(x_request_id: str = 'unset'):
    return x_request_id
class AuditLogger:
    def __call__(self, request_id: str = Depends(get_request_id)):
        return {'logged_request_id': request_id}
@app.get('/audited')
def audited_endpoint(audit: dict = Depends(AuditLogger())):
    return audit
print(client.get('/audited').json())
{'logged_request_id': 'unset'}

Part 8: Overriding a Class Dependency for Testing#

class RealRateLimiter:
    def __call__(self):
        return {'limited': True, 'source': 'real'}
real_limiter = RealRateLimiter()
@app.get('/limited')
def limited_endpoint(status: dict = Depends(real_limiter)):
    return status
print(client.get('/limited').json())
def fake_rate_limiter():
    return {'limited': False, 'source': 'fake'}
app.dependency_overrides[real_limiter] = fake_rate_limiter
print(client.get('/limited').json())
app.dependency_overrides = {}
{'limited': True, 'source': 'real'}
{'limited': False, 'source': 'fake'}

Part 9: Class Dependencies With yield#

class ManagedResource:
    def __init__(self, name: str):
        self.name = name
        self.events = []
    def __call__(self):
        self.events.append(f'{self.name}-open')
        try:
            yield {'resource': self.name}
        finally:
            self.events.append(f'{self.name}-close')
cache_resource = ManagedResource('cache')
@app.get('/cache-check')
def cache_check(resource: dict = Depends(cache_resource)):
    return resource
print(client.get('/cache-check').json())
print(cache_resource.events)
{'resource': 'cache'}
['cache-open', 'cache-close']

Part 10: A Real Pattern - a Configurable Rate Limiter Plus Lookup#

api_app = FastAPI()
class MaxCallsLimiter:
    def __init__(self, max_calls: int):
        self.max_calls = max_calls
        self.calls = 0
    def __call__(self):
        self.calls += 1
        if self.calls > self.max_calls:
            raise HTTPException(status_code=429, detail='rate limit exceeded')
        return {'calls_so_far': self.calls}
from fastapi import HTTPException
cheap_limiter = MaxCallsLimiter(max_calls=2)
PRODUCTS = {1: 'Mug', 2: 'Plate'}
def get_product_name(product_id: int):
    if product_id not in PRODUCTS:
        raise HTTPException(status_code=404, detail='not found')
    return PRODUCTS[product_id]
@api_app.get('/limited-products/{product_id}')
def limited_lookup(name: str = Depends(get_product_name), usage: dict = Depends(cheap_limiter)):
    return {'name': name, 'usage': usage}
api_client = TestClient(api_app)
print(api_client.get('/limited-products/1').json())
print(api_client.get('/limited-products/2').json())
print(api_client.get('/limited-products/1').status_code)
{'name': 'Mug', 'usage': {'calls_so_far': 1}}
{'name': 'Plate', 'usage': {'calls_so_far': 2}}
429

Wrap-Up: What You Learned#

  • A callable class with call works as a dependency, letting init hold configuration.
  • Different instances of the same class can carry different configuration independently.
  • Depends(Class) lets FastAPI instantiate the class itself, treating init like a handler.
  • Depends() with no argument infers the class from the parameter's own type annotation.
  • A bound method on an already-configured instance works as a dependency too.
  • A class dependency can declare its own Depends sub-dependencies, just like a function.
  • dependency_overrides works the same way for class-based dependencies as for functions.
  • A class dependency's call supports yield for setup and teardown, scoped to the instance.
  • Class-based and function-based dependencies combine freely on the same endpoint.
  • That wraps up class-based and sub-dependencies. Next up: APIRouter - Organizing Routes.

Found this useful?

All lessons, notebooks and datasets here are free. If they helped you, a coffee keeps new lessons coming.