Flask Blueprints Deep Dive — Deep Dive
Blueprint internals: the deferred function list
When you define a route on a blueprint, Flask doesn’t touch the application’s URL map. Instead, Blueprint.route() appends a callable to self.deferred_functions. Each callable takes a single argument — a BlueprintSetupState object.
# Simplified internal flow
class Blueprint:
def __init__(self, name, import_name, **kwargs):
self.deferred_functions = []
self.name = name
# ...
def route(self, rule, **options):
def decorator(f):
endpoint = options.pop("endpoint", f.__name__)
self.deferred_functions.append(
lambda state: state.app.add_url_rule(
state.url_prefix + rule,
f"{state.name_prefix}{self.name}.{endpoint}",
f,
**options
)
)
return f
return decorator
The BlueprintSetupState captures the registration context: the app, the URL prefix, the subdomain, and any options passed to register_blueprint. This is why the same blueprint instance can serve different prefixes — each registration creates a fresh state object.
BlueprintSetupState in detail
class BlueprintSetupState:
def __init__(self, blueprint, app, options, first_registration):
self.app = app
self.blueprint = blueprint
self.url_prefix = options.get('url_prefix', blueprint.url_prefix)
self.subdomain = options.get('subdomain', blueprint.subdomain)
self.url_defaults = dict(blueprint.url_values_defaults)
self.url_defaults.update(options.get('url_defaults', {}))
self.name_prefix = options.get('name_prefix', '')
The first_registration flag matters for blueprints that register one-time resources like template paths. Without it, re-registering a blueprint would duplicate template search paths.
Multi-registration patterns
Registering the same blueprint multiple times enables API versioning without code duplication:
from myapp.api import api_bp
app.register_blueprint(api_bp, url_prefix='/api/v1', name='api_v1')
app.register_blueprint(api_bp, url_prefix='/api/v2', name='api_v2')
Since Flask 2.0, you must supply a unique name for each registration. Previously this silently overwrote endpoints, causing hard-to-trace 404s.
Tradeoff: Multi-registration shares the same view functions. If v2 needs different behavior, you need conditional logic inside views or separate blueprints. For non-trivial API evolution, separate blueprints with shared service layers work better.
Nested blueprints: recursive registration
Flask 2.0 introduced Blueprint.register_blueprint(). When a parent registers with the app, it recursively registers children, prepending prefixes:
api = Blueprint('api', __name__, url_prefix='/api')
users = Blueprint('users', __name__, url_prefix='/users')
orders = Blueprint('orders', __name__, url_prefix='/orders')
api.register_blueprint(users) # /api/users
api.register_blueprint(orders) # /api/orders
app.register_blueprint(api)
Internally, this nests BlueprintSetupState objects. The child’s deferred functions receive a state where url_prefix is the concatenation of parent and child prefixes, and name_prefix includes the parent’s name (e.g., api.users.list_users).
Edge case: Circular nesting (A registers B, B registers A) doesn’t raise an error — it creates infinite recursion. Flask doesn’t detect this at registration time, so defensive coding is essential.
Template resolution order
Flask’s Jinja2 loader searches template directories in this order:
- App-level
templates/directory - Blueprint template directories, in registration order
This means app templates shadow blueprint templates. For third-party blueprints (like Flask-Admin), you can override any template by placing a file at the same relative path in your app’s template folder.
Within a blueprint template, you can use subdirectories to avoid collisions:
bp = Blueprint('admin', __name__, template_folder='templates')
# Stores templates in admin/templates/admin/dashboard.html
# Renders with: render_template('admin/dashboard.html')
The double-nesting convention (admin/templates/admin/) prevents name collisions between blueprints that both define index.html.
Error handler scoping
Blueprint error handlers only trigger for errors raised within that blueprint’s views:
@admin_bp.errorhandler(404)
def admin_not_found(e):
return render_template('admin/404.html'), 404
If a 404 occurs outside any blueprint (e.g., a URL that matches no route at all), only app-level error handlers fire. This is a common source of confusion — blueprint 404 handlers don’t catch “page not found” for URLs outside their prefix.
For comprehensive error handling, register handlers at both the blueprint and app level:
@app.errorhandler(404)
def global_not_found(e):
return render_template('404.html'), 404
@api_bp.errorhandler(404)
def api_not_found(e):
return jsonify(error='not found'), 404
Production structuring strategies
Feature-based layout
myapp/
├── auth/
│ ├── __init__.py # Blueprint definition
│ ├── routes.py # View functions
│ ├── models.py # Auth-specific models
│ ├── forms.py # WTForms classes
│ ├── services.py # Business logic
│ └── templates/auth/
├── billing/
│ ├── __init__.py
│ ├── routes.py
│ ├── models.py
│ ├── services.py
│ └── templates/billing/
├── core/
│ ├── __init__.py # App factory
│ ├── extensions.py # db, migrate, login_manager
│ └── templates/ # Base templates, error pages
└── config.py
Each feature directory is a self-contained blueprint with its own models, services, and templates. The core/extensions.py pattern avoids circular imports — extensions are instantiated without an app, then initialized in the factory.
Extension initialization without circular imports
# core/extensions.py
from flask_sqlalchemy import SQLAlchemy
from flask_migrate import Migrate
db = SQLAlchemy()
migrate = Migrate()
# core/__init__.py
def create_app(config_name='default'):
app = Flask(__name__)
app.config.from_object(configs[config_name])
db.init_app(app)
migrate.init_app(app, db)
from myapp.auth import auth_bp
from myapp.billing import billing_bp
app.register_blueprint(auth_bp)
app.register_blueprint(billing_bp, url_prefix='/billing')
return app
Blueprints import db from extensions.py, not from the app. This breaks the circular dependency chain that plagues naive Flask projects.
Blueprint lifecycle hooks in depth
The full hook execution order for a blueprint request:
app.before_request → runs for every request
bp.before_request → runs only for this blueprint's routes
app.before_request (url_value_preprocessor)
bp.url_value_preprocessor → can extract URL values
─── view function ───
bp.after_request → runs only for this blueprint
app.after_request → runs for every request
bp.teardown_request → cleanup, even on errors
app.teardown_request → global cleanup
Blueprint url_value_preprocessor is particularly useful for multi-tenant apps:
@bp.url_value_preprocessor
def extract_tenant(endpoint, values):
g.tenant = values.pop('tenant', None)
@bp.url_defaults
def inject_tenant(endpoint, values):
values.setdefault('tenant', g.tenant)
This pair automatically extracts and re-injects a tenant parameter across all the blueprint’s URLs, keeping view functions clean.
Testing blueprints in isolation
Blueprints can be tested without the full application by creating a minimal test app:
import pytest
from flask import Flask
from myapp.auth import auth_bp
from myapp.core.extensions import db
@pytest.fixture
def app():
app = Flask(__name__)
app.config['TESTING'] = True
app.config['SQLALCHEMY_DATABASE_URI'] = 'sqlite:///:memory:'
db.init_app(app)
app.register_blueprint(auth_bp)
with app.app_context():
db.create_all()
yield app
db.drop_all()
def test_login_page(app):
client = app.test_client()
resp = client.get('/login')
assert resp.status_code == 200
This tests the blueprint’s routes without loading unrelated blueprints or their dependencies, keeping test suites fast and focused.
Performance considerations
Blueprint registration is a startup-time cost, not a per-request cost. Once registered, blueprint routes are indistinguishable from app routes in the URL map. There’s no per-request overhead for using blueprints versus defining routes directly on the app.
However, each before_request hook adds to every request’s processing chain. Ten blueprints each with a before_request hook means ten function calls per request — even though only one blueprint’s hook is “relevant.” Flask calls all app-level hooks for every request; only blueprint-level hooks are scoped.
One thing to remember: The power of blueprints lives in the deferred function list and the setup state object. Every advanced pattern — multi-registration, nesting, prefix stacking — flows from that simple mechanism of recording actions now and replaying them later against a specific application context.
See Also
- Python Django Admin Get an intuitive feel for Django Admin so Python behavior stops feeling unpredictable.
- Python Django Basics Get an intuitive feel for Django Basics so Python behavior stops feeling unpredictable.
- Python Django Celery Integration Why your Django app needs a helper to handle slow jobs in the background.
- Python Django Channels Websockets How Django can send real-time updates to your browser without you refreshing the page.
- Python Django Custom Management Commands How to teach Django new tricks by creating your own command-line shortcuts.