Flask Blueprints, Factories, and Production Layout
1 · The lesson
readThe single-file app.py from earlier lessons works well while the app is small. But when you have about twenty routes, three forms and six models, and you need different configs for tests and production, one big file becomes hard to work with. The imports get tangled. The same app global is used in six places. You can't test with a different database without monkey-patching.
This lesson covers the layout that larger Flask projects use: the application factory pattern, blueprints for modular routing, config classes for environment switching, testing with pytest, and production deployment with gunicorn. Most serious Flask projects end up with this structure.
Run locally with
pip install flask flask-sqlalchemy flask-migrate gunicorn(waitress on Windows). Expected output is shown in comments. The production server (gunicorn) runs only on Linux/macOS. On Windows, usewaitress.
1. The Problem with One Big app.py
A single-file Flask app has four problems as it grows:
- Circular imports. Your
models.pywantsdbfromapp.py. Yourapp.pywantsUserfrommodels.py. Each file imports the other, and Python raises anImportError. - One global
app. You can't easily create a second one for tests with a different config, because importing the module already creates the app. - No separation of concerns. Auth routes, billing routes, and admin routes all share one file. When twenty people edit the same file, you get many merge conflicts.
- Config is hard-coded. Without a clear structure, "use Postgres in production and SQLite in tests" turns into messy, hard-to-follow code.
The fix is two patterns working together: blueprints to split routes into modules, and the application factory to build the app on demand.
2. The Recommended Layout
myapp/ ├── __init__.py # contains the factory function create_app() ├── config.py # Config, DevelopmentConfig, ProductionConfig, TestingConfig ├── extensions.py # db = SQLAlchemy(), migrate = Migrate(), and so on ├── models/ │ ├── __init__.py │ ├── user.py │ └── post.py ├── blueprints/ │ ├── __init__.py │ ├── main/ │ │ ├── __init__.py # bp = Blueprint("main", __name__) │ │ ├── routes.py │ │ └── templates/main/ │ ├── auth/ │ │ ├── __init__.py # bp = Blueprint("auth", __name__, url_prefix="/auth") │ │ ├── routes.py │ │ ├── forms.py │ │ └── templates/auth/ │ └── api/ │ ├── __init__.py │ └── routes.py ├── templates/ │ └── base.html # shared layout ├── static/ │ ├── style.css │ └── app.js ├── tests/ │ ├── conftest.py │ ├── test_main.py │ └── test_auth.py ├── migrations/ # created by `flask db init` ├── wsgi.py # production entrypoint: `app = create_app()` └── requirements.txt
You can change the names. What matters is the structure:
__init__.pyis the entry point. Nothing runs at import time except simple assignments.- Extensions (
db,migrate,login_manager, ...) are module-level objects, created once and not yet initialised.init_app(app)runs inside the factory. - Each blueprint is a self-contained folder: routes, forms, templates for that feature.
3. The Application Factory
# myapp/__init__.py from flask import Flask from .extensions import db, migrate, csrf from .config import Config def create_app(config_class=Config): app = Flask(__name__) app.config.from_object(config_class) # Initialise extensions against this specific app db.init_app(app) migrate.init_app(app, db) csrf.init_app(app) # Register blueprints from .blueprints.main import bp as main_bp from .blueprints.auth import bp as auth_bp from .blueprints.api import bp as api_bp app.register_blueprint(main_bp) app.register_blueprint(auth_bp) app.register_blueprint(api_bp, url_prefix="/api") # Error handlers (could also live in a blueprint) @app.errorhandler(404) def not_found(_e): from flask import render_template return render_template("errors/404.html"), 404 return app
What changed:
create_app()is a function. Call it to get a fresh app. Tests can call it with a different config. Multi-tenant deployments can call it per tenant.- Imports are inside the function.
from .blueprints.main import bpruns lazily, afterdbhas been initialised. No circular-import errors. config_class=Config: pick the config you want.Configis the default, and tests passTestingConfig.
# myapp/extensions.py from flask_sqlalchemy import SQLAlchemy from flask_migrate import Migrate from flask_wtf.csrf import CSRFProtect from sqlalchemy.orm import DeclarativeBase class Base(DeclarativeBase): pass db = SQLAlchemy(model_class=Base) migrate = Migrate() csrf = CSRFProtect()
Extensions are created once, at module level. They are then initialised with each app that the factory creates. This is the pattern that makes the factory work.
4. Defining a Blueprint
A blueprint is like a small Flask app: a place to register routes, error handlers, template folders, and static folders that get attached to the real app later.
# myapp/blueprints/auth/__init__.py from flask import Blueprint bp = Blueprint( "auth", # the blueprint name, which url_for("auth.login") uses __name__, url_prefix="/auth", # every route in this blueprint starts with /auth template_folder="templates", # templates that belong to this blueprint ) from . import routes # noqa: E402 (importing routes registers them)
# myapp/blueprints/auth/routes.py from flask import render_template, redirect, url_for, flash from . import bp from .forms import LoginForm @bp.get("/login") @bp.post("/login") def login(): form = LoginForm() if form.validate_on_submit(): # ... authenticate ... flash("Logged in.", "success") return redirect(url_for("main.home")) # note: dotted blueprint name return render_template("auth/login.html", form=form) @bp.get("/logout") def logout(): # ... log out ... return redirect(url_for("main.home"))
Three things to note:
@bp.route(or@bp.get,@bp.post) instead of@app.route. They work the same way.- The route
/loginbecomes/auth/loginbecause of the blueprint'surl_prefix. url_for("auth.login"): the dotted form is the blueprint name plus the view name. If you get this wrong, Flask raisesBuildError.
5. Configuration Management
# myapp/config.py import os from pathlib import Path basedir = Path(__file__).resolve().parent.parent class Config: """Default config. The other configs build on it.""" SECRET_KEY = os.environ.get("SECRET_KEY", "dev-only-do-not-use-in-prod") SQLALCHEMY_DATABASE_URI = os.environ.get("DATABASE_URL", f"sqlite:///{basedir / 'app.db'}") SQLALCHEMY_TRACK_MODIFICATIONS = False MAX_CONTENT_LENGTH = 5 * 1024 * 1024 class DevelopmentConfig(Config): DEBUG = True SQLALCHEMY_ECHO = True # log SQL class TestingConfig(Config): TESTING = True SQLALCHEMY_DATABASE_URI = "sqlite:///:memory:" # fresh DB per test session WTF_CSRF_ENABLED = False # let tests POST without CSRF tokens class ProductionConfig(Config): DEBUG = False # SECRET_KEY must come from env; if missing, fail loudly SECRET_KEY = os.environ.get("SECRET_KEY") # create_app() refuses to start without it config_map = { "development": DevelopmentConfig, "testing": TestingConfig, "production": ProductionConfig, }
setup added so this can run · defines
import os # noqa: F401 os.environ.setdefault("SECRET_KEY", "example-secret-key")
The factory picks one:
def create_app(config_name=None): config_name = config_name or os.environ.get("FLASK_ENV", "development") config_class = config_map[config_name] app = Flask(__name__) app.config.from_object(config_class) if config_name == "production" and not app.config["SECRET_KEY"]: raise RuntimeError("SECRET_KEY must be set in production") ...
setup added so this can run · defines config_map, Flask, os
# Lightweight mock for objects whose attributes/methods aren't critical class _AutoMock: def __init__(self, name='mock'): self._name = name def __getattr__(self, k): return _AutoMock(self._name + '.' + k) def __call__(self, *a, **kw): print('-> ' + self._name + '() called') return _AutoMock(self._name + '()') def __repr__(self): return '<mock ' + self._name + '>' def __str__(self): return '<mock ' + self._name + '>' def __bool__(self): return True def __iter__(self): return iter([]) def __len__(self): return 0 def __getitem__(self, k): return _AutoMock(self._name + '[...]') def __setitem__(self, k, v): pass def __enter__(self): return self def __exit__(self, *a): return False async def __aenter__(self): return self async def __aexit__(self, *a): return False def __add__(self, o): return self def __radd__(self, o): return self def __sub__(self, o): return self def __mul__(self, o): return self def __rmul__(self, o): return self def __truediv__(self, o): return self def __eq__(self, o): return isinstance(o, _AutoMock) def __hash__(self): return hash(self._name) def __lt__(self, o): return True def __le__(self, o): return True def __gt__(self, o): return False def __ge__(self, o): return False def __mro_entries__(self, bases): return (object,) config_map = _AutoMock('config_map') def Flask(*_a, **_kw): print('-> Flask() called') return _AutoMock('Flask()') os = _AutoMock('os')
Three rules:
- Production fails loudly on missing secrets.
create_app("production")raises an error ifSECRET_KEYis unset, and that is what you want. (A bareos.environ["SECRET_KEY"]in the class body would break dev and test runs too, because every config class runs at import.) Don't fall back to a dev default in prod. - Tests get their own config. In-memory SQLite, CSRF off, debug off (debug-on tests can mask bugs).
- Never commit
.envfiles. Secrets live in env vars, set by your deployment platform. See devops-secrets.
6. The g and current_app Globals
Inside a request, current_app is the running Flask instance (the one the factory built), and g is a per-request scratchpad:
from flask import current_app, g, request @bp.before_request def load_user(): token = request.headers.get("Authorization") g.user = lookup_user(token) if token else None @bp.get("/profile") def profile(): if g.user is None: return "unauth", 401 return render_template("profile.html", user=g.user, app_name=current_app.config["APP_NAME"])
setup added so this can run · defines bp, render_template, lookup_user
# Lightweight mock for objects whose attributes/methods aren't critical class _AutoMock: def __init__(self, name='mock'): self._name = name def __getattr__(self, k): return _AutoMock(self._name + '.' + k) def __call__(self, *a, **kw): print('-> ' + self._name + '() called') return _AutoMock(self._name + '()') def __repr__(self): return '<mock ' + self._name + '>' def __str__(self): return '<mock ' + self._name + '>' def __bool__(self): return True def __iter__(self): return iter([]) def __len__(self): return 0 def __getitem__(self, k): return _AutoMock(self._name + '[...]') def __setitem__(self, k, v): pass def __enter__(self): return self def __exit__(self, *a): return False async def __aenter__(self): return self async def __aexit__(self, *a): return False def __add__(self, o): return self def __radd__(self, o): return self def __sub__(self, o): return self def __mul__(self, o): return self def __rmul__(self, o): return self def __truediv__(self, o): return self def __eq__(self, o): return isinstance(o, _AutoMock) def __hash__(self): return hash(self._name) def __lt__(self, o): return True def __le__(self, o): return True def __gt__(self, o): return False def __ge__(self, o): return False def __mro_entries__(self, bases): return (object,) bp = _AutoMock('bp') def render_template(*_a, **_kw): print('-> render_template() called') return _AutoMock('render_template()') def lookup_user(*_a, **_kw): print('-> lookup_user() called') return _AutoMock('lookup_user()')
glives for one request. Use it to store per-request state, such as the current user, a DB session or a snapshot of feature flags.current_applets blueprints access app config without importing the app directly (which would re-create the circular-import problem the factory was meant to solve).
Outside a request (in scripts and tests), push a context yourself:
from myapp import create_app app = create_app() with app.app_context(): from myapp.extensions import db db.create_all() # works because we're in an app context
7. Cross-Blueprint URL Building
The dotted form again, url_for("blueprint.view"):
# In a route or template inside the auth blueprint: return redirect(url_for("main.home")) # a route in another blueprint return redirect(url_for(".login")) # shortcut for a route in the same blueprint # In a template: <a href="{{ url_for('api.users') }}">API</a>
A leading . means "this blueprint". url_for(".login") inside auth resolves to auth.login. This is useful when you rename the blueprint.
8. Testing with pytest
app.test_client() is the in-process HTTP client. You don't need a real server, so tests run fast:
# tests/conftest.py import pytest from myapp import create_app from myapp.extensions import db @pytest.fixture def app(): app = create_app("testing") with app.app_context(): db.create_all() yield app db.session.remove() db.drop_all() @pytest.fixture def client(app): return app.test_client()
# tests/test_main.py def test_home_loads(client): r = client.get("/") assert r.status_code == 200 assert b"Items" in r.data def test_create_item_redirects(client): r = client.post("/items/new", data={"name": "test"}, follow_redirects=False) assert r.status_code == 302 assert r.headers["Location"].endswith("/") def test_create_item_persists(client, app): client.post("/items/new", data={"name": "first"}) with app.app_context(): from myapp.models import Item from sqlalchemy import select items = db.session.execute(select(Item)).scalars().all() assert len(items) == 1 assert items[0].name == "first"
setup added so this can run · defines db
# Lightweight mock for objects whose attributes/methods aren't critical class _AutoMock: def __init__(self, name='mock'): self._name = name def __getattr__(self, k): return _AutoMock(self._name + '.' + k) def __call__(self, *a, **kw): print('-> ' + self._name + '() called') return _AutoMock(self._name + '()') def __repr__(self): return '<mock ' + self._name + '>' def __str__(self): return '<mock ' + self._name + '>' def __bool__(self): return True def __iter__(self): return iter([]) def __len__(self): return 0 def __getitem__(self, k): return _AutoMock(self._name + '[...]') def __setitem__(self, k, v): pass def __enter__(self): return self def __exit__(self, *a): return False async def __aenter__(self): return self async def __aexit__(self, *a): return False def __add__(self, o): return self def __radd__(self, o): return self def __sub__(self, o): return self def __mul__(self, o): return self def __rmul__(self, o): return self def __truediv__(self, o): return self def __eq__(self, o): return isinstance(o, _AutoMock) def __hash__(self): return hash(self._name) def __lt__(self, o): return True def __le__(self, o): return True def __gt__(self, o): return False def __ge__(self, o): return False def __mro_entries__(self, bases): return (object,) db = _AutoMock('db')
A few patterns:
- Fresh app per test. The
appfixture creates a new app for each test (it is not session-scoped), so tests can't affect each other. - CSRF off in TestingConfig. Tests post forms directly without rendering them first.
with app.app_context():in assertions that touch the DB outside the client call.
Run with pytest. The whole suite for a small app finishes in under a second.
9. Production WSGI Servers
The development server (app.run() / flask run) is not built for speed or security, so don't use it in production. Use a production server instead:
Gunicorn, the standard on Linux/macOS:
pip install gunicorn
# wsgi.py
from myapp import create_app
app = create_app("production")
# Start it
gunicorn --workers 4 --bind 0.0.0.0:8000 wsgi:app
# Or directly call the factory
gunicorn --workers 4 --bind 0.0.0.0:8000 'myapp:create_app()'Waitress, written in pure Python. It works on Windows because it does not need fork:
pip install waitress waitress-serve --listen=0.0.0.0:8000 wsgi:app
Worker count rule of thumb: (2 × CPU cores) + 1. Each worker is a separate process, and workers share no memory. CPU-bound work gets faster with more workers, up to the number of CPU cores. I/O-bound work benefits more from async workers (gunicorn -k gevent or switching to FastAPI).
10. nginx in Front of Gunicorn
In production you put nginx (or another reverse proxy) in front of Gunicorn. nginx handles:
- TLS termination: HTTPS connections end at nginx, and nginx talks plain HTTP to gunicorn on localhost.
- Static files: nginx serves
static/and uploaded media directly, without Python. - Connection buffering: clients on slow networks don't keep Python workers busy.
- Compression, request limits, IP allowlists, request logging.
The shape:
Client → nginx :443 (TLS, static) → gunicorn :8000 (Python) → Postgres :5432
A minimal nginx server block:
server {
listen 443 ssl;
server_name example.com;
ssl_certificate /etc/letsencrypt/live/example.com/fullchain.pem;
ssl_certificate_key /etc/letsencrypt/live/example.com/privkey.pem;
location /static/ {
alias /srv/myapp/static/;
expires 30d;
}
location / {
proxy_pass http://127.0.0.1:8000;
proxy_set_header Host $host;
proxy_set_header X-Real-IP $remote_addr;
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
proxy_set_header X-Forwarded-Proto $scheme;
}
}Or skip the nginx setup and use a managed platform such as Fly.io, Railway, Render or AWS App Runner. They terminate TLS, run your container, and serve static assets without you writing nginx config. See devops-docker and devops-environments.
11. Common Mistakes
1. Circular imports
Symptom: ImportError: cannot import name 'db' from partially initialized module 'myapp.extensions'. Cause: models.py imports db at module top, and something db depends on imports models.py. Fix: keep extensions in extensions.py, import blueprints inside create_app(), and import models inside view functions if necessary.
2. Calling db.init_app(app) in a global
# Wrong: this runs at import time, before a real app config exists db.init_app(some_global_app)
setup added so this can run · defines some_global_app, db
# Lightweight mock for objects whose attributes/methods aren't critical class _AutoMock: def __init__(self, name='mock'): self._name = name def __getattr__(self, k): return _AutoMock(self._name + '.' + k) def __call__(self, *a, **kw): print('-> ' + self._name + '() called') return _AutoMock(self._name + '()') def __repr__(self): return '<mock ' + self._name + '>' def __str__(self): return '<mock ' + self._name + '>' def __bool__(self): return True def __iter__(self): return iter([]) def __len__(self): return 0 def __getitem__(self, k): return _AutoMock(self._name + '[...]') def __setitem__(self, k, v): pass def __enter__(self): return self def __exit__(self, *a): return False async def __aenter__(self): return self async def __aexit__(self, *a): return False def __add__(self, o): return self def __radd__(self, o): return self def __sub__(self, o): return self def __mul__(self, o): return self def __rmul__(self, o): return self def __truediv__(self, o): return self def __eq__(self, o): return isinstance(o, _AutoMock) def __hash__(self): return hash(self._name) def __lt__(self, o): return True def __le__(self, o): return True def __gt__(self, o): return False def __ge__(self, o): return False def __mro_entries__(self, bases): return (object,) some_global_app = _AutoMock('some_global_app') db = _AutoMock('db')
Always call init_app() inside create_app(), where the right app and config exist. The main purpose of the factory is to connect extensions to the app later, not at import time.
3. Inconsistent url_prefix
auth_bp = Blueprint("auth", __name__, url_prefix="/auth") # and also app.register_blueprint(auth_bp, url_prefix="/auth") # this value replaces the blueprint's: still /auth/...
setup added so this can run · defines Blueprint, app
# Lightweight mock for objects whose attributes/methods aren't critical class _AutoMock: def __init__(self, name='mock'): self._name = name def __getattr__(self, k): return _AutoMock(self._name + '.' + k) def __call__(self, *a, **kw): print('-> ' + self._name + '() called') return _AutoMock(self._name + '()') def __repr__(self): return '<mock ' + self._name + '>' def __str__(self): return '<mock ' + self._name + '>' def __bool__(self): return True def __iter__(self): return iter([]) def __len__(self): return 0 def __getitem__(self, k): return _AutoMock(self._name + '[...]') def __setitem__(self, k, v): pass def __enter__(self): return self def __exit__(self, *a): return False async def __aenter__(self): return self async def __aexit__(self, *a): return False def __add__(self, o): return self def __radd__(self, o): return self def __sub__(self, o): return self def __mul__(self, o): return self def __rmul__(self, o): return self def __truediv__(self, o): return self def __eq__(self, o): return isinstance(o, _AutoMock) def __hash__(self): return hash(self._name) def __lt__(self, o): return True def __le__(self, o): return True def __gt__(self, o): return False def __ge__(self, o): return False def __mro_entries__(self, bases): return (object,) def Blueprint(*_a, **_kw): print('-> Blueprint() called') return _AutoMock('Blueprint()') app = _AutoMock('app')
Pick one place to set the prefix, usually on the blueprint, not on the registration. Setting both doesn't double the prefix. The registration value replaces the blueprint's value without any warning, so changing the blueprint later does nothing.
4. Forgetting app.app_context() in scripts
# A standalone script that touches the DB from myapp import create_app from myapp.extensions import db app = create_app() db.create_all() # RuntimeError: no application context
Wrap any DB call in with app.app_context():. Inside a request, Flask pushes the context for you. Outside a request, you do it yourself.
5. Hardcoded blueprint names in url_for
url_for("login") # Wrong: the name needs the blueprint prefix url_for("auth.login") # RIGHT url_for(".login") # also right if you're inside auth
setup added so this can run · defines url_for
# Lightweight mock for objects whose attributes/methods aren't critical class _AutoMock: def __init__(self, name='mock'): self._name = name def __getattr__(self, k): return _AutoMock(self._name + '.' + k) def __call__(self, *a, **kw): print('-> ' + self._name + '() called') return _AutoMock(self._name + '()') def __repr__(self): return '<mock ' + self._name + '>' def __str__(self): return '<mock ' + self._name + '>' def __bool__(self): return True def __iter__(self): return iter([]) def __len__(self): return 0 def __getitem__(self, k): return _AutoMock(self._name + '[...]') def __setitem__(self, k, v): pass def __enter__(self): return self def __exit__(self, *a): return False async def __aenter__(self): return self async def __aexit__(self, *a): return False def __add__(self, o): return self def __radd__(self, o): return self def __sub__(self, o): return self def __mul__(self, o): return self def __rmul__(self, o): return self def __truediv__(self, o): return self def __eq__(self, o): return isinstance(o, _AutoMock) def __hash__(self): return hash(self._name) def __lt__(self, o): return True def __le__(self, o): return True def __gt__(self, o): return False def __ge__(self, o): return False def __mro_entries__(self, bases): return (object,) def url_for(*_a, **_kw): print('-> url_for() called') return _AutoMock('url_for()')
The form without the blueprint name raises BuildError once the route is in a blueprint. Don't forget the dot.
6. Tests that share state
If your app fixture is session-scoped and tests create rows, test 5 can see test 3's data. Use function scope (default) for the app and db fixtures, or wrap each test in a transaction that gets rolled back. The second way is faster on big test suites. Search for "pytest savepoint" to find the pattern.
7. Running gunicorn with debug or app.run() in production
Both serve traffic, but neither is right for production. app.run(debug=True) exposes the Werkzeug debugger, which lets a visitor run Python code on your server. app.run(debug=False) is still the development server, which is not built for production traffic. Use gunicorn (or waitress on Windows) for any deployment a real user touches. See security-checklist.
🎯 Your Turn: Refactor to Factory + Blueprints
Take this single-file app:
# old_app.py (the code you start from) from flask import Flask, render_template, request, redirect, url_for app = Flask(__name__) app.config["SECRET_KEY"] = "dev" users = {} next_uid = 1 @app.get("/") def home(): return render_template("home.html", count=len(users)) @app.get("/users/") def list_users(): return render_template("users_list.html", users=users.values()) @app.post("/users/") def create_user(): global next_uid name = request.form.get("name", "").strip() if name: users[next_uid] = {"id": next_uid, "name": name} next_uid += 1 return redirect(url_for("list_users")) if __name__ == "__main__": app.run(debug=True)
Refactor into:
myapp/ ├── __init__.py # create_app() factory ├── config.py # Config + DevelopmentConfig + TestingConfig ├── extensions.py # (empty for now, because there is no DB yet) ├── blueprints/ │ ├── __init__.py │ ├── main/ │ │ ├── __init__.py # bp = Blueprint("main", __name__) │ │ └── routes.py │ └── users/ │ ├── __init__.py # bp = Blueprint("users", __name__, url_prefix="/users") │ └── routes.py └── templates/ ├── base.html ├── home.html └── users_list.html
Requirements:
create_app()accepts aconfig_class(or name).mainblueprint owns/.usersblueprint owns/users/withGETandPOST. The blueprint setsurl_prefix="/users"so the routes inside it are"/"(which becomes/users/).- All
url_forcalls in templates use the dotted form. - A
wsgi.pyat the project root that creates the app for gunicorn.
Hint 1: Routes inside a blueprint
A blueprint withurl_prefix="/users" wants routes like @bp.route("/") (which becomes /users/). Don't write @bp.route("/users/"), because that would become /users/users/. The route inside the blueprint is relative to the prefix.
Hint 2: Templates and url_for in the new layout
Templates can live in a top-leveltemplates/ folder (shared), and Flask finds them. In templates, references to list_users become url_for("users.list_users"). References to home become url_for("main.home"). If you forget the prefix, you'll get BuildError on the first request.
Show full solution
# myapp/__init__.py from flask import Flask from .config import Config def create_app(config_class=Config): app = Flask(__name__) app.config.from_object(config_class) if config_class.__name__ == "ProductionConfig" and not app.config["SECRET_KEY"]: raise RuntimeError("SECRET_KEY must be set in production") from .blueprints.main import bp as main_bp from .blueprints.users import bp as users_bp app.register_blueprint(main_bp) app.register_blueprint(users_bp) return app
# myapp/config.py import os class Config: SECRET_KEY = os.environ.get("SECRET_KEY", "dev-only") class DevelopmentConfig(Config): DEBUG = True class TestingConfig(Config): TESTING = True WTF_CSRF_ENABLED = False class ProductionConfig(Config): DEBUG = False SECRET_KEY = os.environ.get("SECRET_KEY") # create_app() refuses to start without it
setup added so this can run · defines
import os # noqa: F401 os.environ.setdefault("SECRET_KEY", "example-secret-key")
# myapp/extensions.py # Empty for now, because this exercise has no SQLAlchemy. Real apps fill this file.
# myapp/blueprints/main/__init__.py from flask import Blueprint bp = Blueprint("main", __name__) from . import routes # noqa: E402
# myapp/blueprints/main/routes.py from flask import render_template from . import bp from ..users.store import users # a tiny shared store (shown below) @bp.get("/") def home(): return render_template("home.html", count=len(users))
# myapp/blueprints/users/__init__.py from flask import Blueprint bp = Blueprint("users", __name__, url_prefix="/users") from . import routes # noqa: E402
# myapp/blueprints/users/store.py # In a real app this would be replaced by a SQLAlchemy model. users: dict[int, dict] = {} _next_uid = [1] def add_user(name: str) -> dict: uid = _next_uid[0] _next_uid[0] += 1 users[uid] = {"id": uid, "name": name} return users[uid]
# myapp/blueprints/users/routes.py from flask import render_template, request, redirect, url_for from . import bp from .store import users, add_user @bp.get("/") def list_users(): return render_template("users_list.html", users=users.values()) @bp.post("/") def create_user(): name = (request.form.get("name") or "").strip() if name: add_user(name) return redirect(url_for("users.list_users"))
<!-- myapp/templates/base.html -->
<!doctype html>
<html>
<head><title>{% block title %}MyApp{% endblock %}</title></head>
<body>
<nav>
<a href="{{ url_for('main.home') }}">Home</a> |
<a href="{{ url_for('users.list_users') }}">Users</a>
</nav>
<hr>
{% block content %}{% endblock %}
</body>
</html><!-- myapp/templates/home.html -->
{% extends "base.html" %}
{% block content %}
<h1>Home</h1>
<p>{{ count }} users registered.</p>
{% endblock %}<!-- myapp/templates/users_list.html -->
{% extends "base.html" %}
{% block content %}
<h1>Users</h1>
{% if users %}
<ul>{% for u in users %}<li>{{ u.name }}</li>{% endfor %}</ul>
{% else %}
<p>No users yet.</p>
{% endif %}
<form method="post" action="{{ url_for('users.create_user') }}">
<input name="name" placeholder="Name" required>
<button>Add</button>
</form>
{% endblock %}# wsgi.py (the production entry point) from myapp import create_app from myapp.config import ProductionConfig app = create_app(ProductionConfig)
Run development:
export FLASK_APP=myapp # PowerShell: $env:FLASK_APP="myapp" flask run
Run production:
gunicorn --workers 4 --bind 0.0.0.0:8000 wsgi:app # Windows: waitress-serve --listen=0.0.0.0:8000 wsgi:app
What this gets right:
- Factory pattern:
create_app(config_class). Tests passTestingConfig, production passesProductionConfig, dev uses the default. - Two blueprints, two namespaces.
main.homeandusers.list_userscan never collide. A new feature gets a new blueprint folder, so nobody has to edit a shared file. - Templates use the dotted form. Every route is clearly named by blueprint, and Flask raises
BuildErrorif a name is wrong. - Production secret comes from env.
ProductionConfigraises an error at once ifSECRET_KEYis missing. This is better than running with a weak default. wsgi.pyfor gunicorn. Production servers point to a stable, importable WSGI app object.
What's missing for a real app:
- A real database: see flask-database for SQLAlchemy + Flask-Migrate.
usersas a dict is a stand-in. - Auth: auth-passwords, auth-jwt.
- Tests:
app.test_client()with aclientfixture, as shown in Section 8. - Dockerfile + CI: devops-docker, devops-github-actions.
- Logging, metrics, error tracking. The factory is where you set up those extensions.
What You Learned
- Single-file Flask apps get hard to manage after a few routes. Move to the factory + blueprint layout early, because refactoring later is harder than starting that way.
- Blueprints are like small Flask apps, with their own routes, templates, and
url_prefix. Each owns one feature area (auth, billing, api, admin). - The application factory,
create_app(config_class), builds a fresh app each time you call it. Tests use a different config, and production loads secrets from env vars. - Extensions (
db,migrate,login_manager) live inextensions.pyas module-level uninitialised objects.init_app(app)happens inside the factory. url_for("blueprint.view")with the dotted name. Inside a blueprint,url_for(".view")is a same-blueprint shortcut.gholds per-request state, andcurrent_appis the running app. Both help you avoid circular imports.- Config classes (
Config,DevelopmentConfig,TestingConfig,ProductionConfig) keep environment differences in one file. Production stops with an error if a secret is missing. app.test_client()with pytest gives fast integration tests. CSRF off inTestingConfig. Fresh app per test.- Production: gunicorn (
workers = 2 × cores + 1) or waitress on Windows. nginx in front for TLS, static files and connection buffering, or a managed platform that does those things for you. - Never
app.run()in production. The development server is not built for production traffic, and withdebug=Truea visitor can run code on your server.
You now know the full Flask production stack. From here:
- JSON-first APIs: fastapi-basics is the modern alternative. It is faster, supports async natively, and generates OpenAPI docs automatically.
- Batteries included (many features come built in): django-basics, for projects that want the admin, ORM and auth in one framework.
- Auth: auth-passwords, auth-jwt, auth-oauth.
- Production database: db-postgres, db-orm-patterns.
- Deployment: devops-docker, devops-github-actions, devops-environments, devops-secrets.
Next: FastAPI Basics.
Found this useful? Share it with a friend who’s learning Python.