Articles
Flask Interview Questions: Request Contexts, App Factories, and Test Isolation
Practice Flask request context, app factory, and test isolation questions with precise lifecycle traces and failing-test diagnosis in Python apps.
Flask context errors often appear far from the line that caused them: a test reads request after cleanup, a background worker imports current_app, or two test cases share configuration through a module-level app. Interview questions about Flask are easiest to answer if you trace the lifetime of each object rather than recite decorator names. The central distinction is simple: an application instance holds configuration and registered routes; an application context makes that instance available to current_app and g; a request context makes request and session available for one request.
Official fact: Flask's request-context documentation says a request pushes its request context and, when needed, an application context; teardown follows when contexts are popped. Its application-factory guidance recommends factories when you need differently configured instances. Inference: the most useful interview answer predicts exactly which proxy is valid at each point in a failing test. The examples below are original exercises, not reports of employer-specific questions.
Question 1: What lives in each context?
The Flask application object is created by calling Flask or a factory. It owns configuration, extensions, blueprints, route registration, and other setup. The application context binds one application to a worker's current execution context, so current_app and g can resolve. The request context additionally binds request and session to a particular request. It is possible to have an application context without a request context, such as during a CLI command or a direct unit test. A normal handled request has both.
That gives a useful prediction table:
| Location | current_app and g | request and session | Why |
|---|---|---|---|
| Module import, with no context | Unavailable | Unavailable | No app has been pushed |
| Inside app.app_context() | Available | Unavailable | Only the app context is active |
| Inside app.test_request_context() | Available | Available | Request push also provides app context |
| Inside a test-client request handler | Available | Available | Flask dispatches under both contexts |
| After context teardown | Unavailable unless another context is active | Unavailable | Proxies no longer have that binding |
The word “global” is misleading here. A LocalProxy looks like a global import, but it resolves against the active context, not one process-wide request object. Flask documents its use of Python contextvars and Werkzeug LocalProxy. Do not pass request itself into an unrelated background thread and assume it can resolve there. Pass the required primitive data explicitly. Also do not confuse g with durable storage: it is useful for data during one application-context lifetime, not a persistent cross-request cache.
An interviewer may ask why current_app works inside app.app_context() while request.path raises a RuntimeError. The answer is the missing request context, not a missing route. Create the smallest context the code actually needs. For production code, consider passing explicit dependencies to a pure function instead of manufacturing a request context only to make a test pass.
Question 2: Why does test_request_context not run my authentication hook?
A direct context push makes the request and session proxies usable. It does not by itself dispatch a request through URL matching, before_request handlers, view execution, after_request handlers, and response processing. Flask's testing guide explicitly distinguishes this from a test-client request. That distinction is a common source of a false positive: a helper reads request headers successfully in a unit test, while the real route still fails because the authentication hook, parsing layer, or error handler behaves differently.
Use app.test_request_context() when testing a small function that only needs the proxies, such as extracting a value from request.args. Use app.test_client() when the assertion concerns route matching, hooks, status codes, cookies, or the serialized response. You can manually call preprocess_request in a context for a targeted hook test, as Flask shows, but a client request is usually a clearer integration boundary.
Consider this original exercise. A before_request hook sets g.user from an authorization header, and a view requires g.user. A test that pushes test_request_context with the header, calls the view directly, and asserts success might fail or might pass only because another test contaminated g. It does not prove that the hook ran. The reliable test sends a request through the client, asserts the expected status and body, and separately tests the no-header path. Design matrix-operations HTTP service and test strategy is a useful related practice prompt for deciding which assertions belong at the HTTP boundary.
Question 3: How does an app factory improve isolation?
An application factory is a function that constructs and configures a new Flask instance. In the standard pattern, create_app accepts optional test configuration, initializes extensions against that particular app, registers blueprints, and returns it. Flask's factory pattern documentation explains that this permits multiple differently configured instances in one process. It is not automatically isolated simply because the function is named create_app. Any module-level mutable cache, open connection, or already-bound extension can still leak state between instances.
A minimal testable structure is:
db = SQLAlchemy()
def create_app(test_config=None):
app = Flask(__name__)
app.config.from_mapping(DATABASE_URL="sqlite:///app.db")
if test_config:
app.config.update(test_config)
db.init_app(app)
app.register_blueprint(api)
return app
The example illustrates the binding pattern; SQLAlchemy is an optional extension, not a Flask requirement. The extension object is created unbound, then attached to the new app. A test fixture can call create_app with TESTING and a test database, yield the app, and clean up the database and files afterward. A second fixture can create its own app and client. The important assertion is not that the object IDs differ; it is that one test's config, database writes, and active contexts do not affect another test.
A failure trace makes the risk concrete. Suppose create_app increments a global request counter or registers a blueprint on a shared app object. Test A runs and adds one route; test B calls the factory and unexpectedly sees that route or a stale counter. The fix is to move per-app state into app.config, app.extensions, a context-local value, or an injected dependency with explicit lifetime. You should be able to name which state needs to survive requests and which should be rebuilt per test. Validate Unit-Test Coverage and Identify Missing Scenarios helps turn this diagnosis into assertions rather than relying on one green happy-path test.
Question 4: What is the lifecycle of teardown and cleanup?
For a handled request, Flask pushes contexts, dispatches the request, produces a response, and eventually pops the request and application contexts. Teardown callbacks run as contexts are popped, including when an exception occurred earlier. This is why teardown_request and teardown_appcontext are appropriate places to close request-scoped or application-context-scoped resources. They are not equivalent to after_request: after_request operates on the response path, while teardown is cleanup associated with context removal.
In tests, lifecycle details matter. Flask's test client can preserve contexts inside a with block to let the test inspect request or session after a response; they are popped when the block exits. If you keep a context pushed manually and forget to pop it, later tests may see a proxy bound to the wrong application or request. A context manager is safer than a naked push/pop pair when the test can raise an assertion.
A good interview trace names three observations: what resource was acquired, which context owns it, and what event releases it. For a database connection stored in g, the first request may open it lazily; the teardown callback checks g and closes it; the next request opens its own. If the resource is a process-wide pool, do not close the entire pool per request. Match cleanup scope to ownership scope. Bootstrap Service: Aggregate Downstream Services and Return the Right HTTP Status is a relevant API scenario for thinking about what happens when a downstream call fails during a request.
A worked failure: a test passes alone but fails in the full suite
Imagine a Flask app that reads a feature flag from current_app.config in a helper. One test creates an app with the flag enabled and pushes its app context. Another creates an app with the flag disabled. The second passes in isolation but fails after the first. There are several possible causes: the first context was not popped; the helper cached the value in a module global; the “factory” returned the same app object; or a fixture used session rather than function scope. Guessing “Flask context bug” is not enough.
Trace it in this order. First compare app identity and configuration for each fixture. Second assert that the helper reads the active current_app rather than an imported app singleton. Third inspect any module-level cache and the fixture's lifetime. Fourth make contexts explicit with with app.app_context() and ensure each fixture closes resources. Finally run the two tests in both orders. If only one order fails, you have evidence of leakage; if both fail, recheck the feature-flag logic itself.
The associated regression should assert observable behavior through the route and, separately, the helper under an explicit app context. A direct unit test can prove that the helper reads configuration; a client test proves that a request selects the intended app and returns the right response. Add a negative test that accesses request outside a request context and expects the documented RuntimeError. A test that never exercises the missing-context boundary cannot establish isolation.
A second failure mode involves asynchronous work. A view schedules a background task that reads request.headers after the response returns. The proxy may no longer be bound; even if a development environment makes it appear to work, that is not a safe lifetime contract. Copy the required header value while the request is active and pass it to the task. Do not pass the entire request object merely to postpone a parsing decision. Debug missing output in Python async HTTP flow can help rehearse a related lifetime and observability investigation.
How to answer in the interview
For each Flask scenario, draw two boxes: the application instance and the current execution context. Say which proxy the code touches and which context makes it available. Then identify whether the question concerns a direct helper call or full request dispatch. Finish with a specific test that would fail before your change and pass afterward. That method is clearer than memorizing “use an app factory” as a universal fix.
If asked to design a production endpoint, apply the same boundaries to validation and errors. Parse input at the request boundary, convert it into explicit domain values, call a function that does not secretly depend on request, and return a deliberate status. Design input validation and error handling is a focused practice exercise. Keep the test factory small enough to substitute configuration, but do not hide database cleanup, external calls, or global state behind it.
The practical preparation set is five questions: Which context is active? Was there actual request dispatch? Is state per app, per request, or process-wide? Who closes it? Which test proves the boundary? If you can answer those with a trace, you can handle many Flask interview variants without relying on a brittle list of trivia.