Skip to main content

Application Discovery and ScriptInfo

When you execute a command like flask run or flask shell, Flask must bridge the gap between the command-line interface (built with Click) and your application code. This bridge is managed by the ScriptInfo helper class, which encapsulates the logic for discovering, importing, and initializing a Flask application instance.

The Role of ScriptInfo

ScriptInfo is a state container used internally by Flask's CLI to manage the lifecycle of an application during a command's execution. It is typically instantiated by FlaskGroup (the root Click group for the flask command) and passed through the Click context.

The primary responsibility of ScriptInfo is the load_app() method. This method ensures that the application is only loaded once, even if multiple commands or decorators (like @with_appcontext) require it.

# src/flask/cli.py

def load_app(self) -> Flask:
if self._loaded_app is not None:
return self._loaded_app

app: Flask | None = None
if self.create_app is not None:
app = self.create_app()
else:
# Discovery logic follows...

Application Discovery Logic

Flask employs a tiered strategy to locate an application, moving from explicit configuration to implicit conventions.

Explicit Configuration

If an app_import_path is provided (via the --app option or the FLASK_APP environment variable), ScriptInfo uses it to locate the application. The path can be a simple module name or a module followed by an attribute or factory function call (e.g., myapp.app:create_app("dev")).

Flask uses ast.parse in find_app_by_string to safely evaluate these strings, allowing for factory functions that take literal arguments.

Implicit Discovery

If no path is provided, ScriptInfo searches the current directory for common entry points:

  1. wsgi.py
  2. app.py

For each file, Flask uses prepare_import to calculate the Python path and add the current directory to sys.path if necessary, ensuring the module can be imported correctly even if it isn't in the standard search path.

The Best App Heuristic

When a module is imported without a specific attribute name (e.g., just FLASK_APP=myapp), Flask uses find_best_app to scan the module for:

  1. An attribute named app or application.
  2. Any single instance of the Flask class.
  3. A factory function named create_app or make_app.

If multiple Flask instances are found and none are named app or application, Flask fails with a NoAppException to avoid ambiguity.

Error Handling with NoAppException

The NoAppException is a specialized subclass of click.UsageError. It is designed to provide actionable feedback when discovery fails.

One critical distinction Flask makes is between a module that cannot be found and a module that fails to import due to an internal error. In locate_app, Flask checks the traceback depth:

# src/flask/cli.py

try:
__import__(module_name)
except ImportError:
# If the error happened inside the module, show the full traceback
if sys.exc_info()[2].tb_next:
raise NoAppException(
f"While importing {module_name!r}, an ImportError was raised:\n\n"
f"{traceback.format_exc()}"
) from None
# Otherwise, it's just a missing module
elif raise_if_not_found:
raise NoAppException(f"Could not import {module_name!r}.") from None

This ensures that if your code has a syntax error or a broken import, you see the actual traceback rather than a generic "App not found" message.

CLI Context and Environment

When running through the CLI, Flask sets the FLASK_RUN_FROM_CLI environment variable to "true". This is a safeguard for applications that might call app.run() at the module level without a if __name__ == "__main__": block. When this variable is set, app.run() becomes a no-op, preventing the CLI from accidentally starting a second, blocking development server during the import process.

Additionally, ScriptInfo manages the application's debug state. If set_debug_flag is enabled (the default), load_app will update the application's debug attribute based on the FLASK_DEBUG environment variable or the --debug CLI flag.

Usage in Testing

While ScriptInfo is primarily internal, it is a key component of the FlaskCliRunner. When you use runner.invoke(app.cli, ...), Flask creates a ScriptInfo object to ensure the test command runs against the correct application instance.

# Example of manual ScriptInfo usage in a test context
from flask.cli import ScriptInfo

def test_custom_discovery():
def my_factory():
return Flask("test_app")

info = ScriptInfo(create_app=my_factory)
app = info.load_app()
assert app.name == "test_app"

This manual control allows the testing infrastructure to bypass environment variables and provide a specific application instance or factory directly to the CLI dispatching logic.