Flask is a lightweight WSGI web application framework for Python. Created by Armin Ronacher in 2010, it started as an April Fools’ joke that merged two libraries — Werkzeug (a WSGI toolkit) and Jinja2 (a template engine) — into a single package. The joke turned out to be genuinely useful, and Flask quickly became one of the most popular Python web frameworks in the world.
Flask calls itself a “micro-framework,” but do not let that label fool you. “Micro” means the core is deliberately small and unopinionated. Flask gives you routing, request handling, templates, and a development server. Everything else — database access, form validation, authentication, file uploads — you choose and plug in yourself. This is a feature, not a limitation. It means you understand every piece of your stack, and you never carry dead weight from features you do not use.
The two pillars that Flask is built on deserve a brief mention:
Flask’s philosophy is simple: give developers the tools they need and get out of their way. There is no ORM you are forced to use, no admin panel you never asked for, no project structure you must follow. You make the decisions. This tutorial will take you from zero to a working Flask application, covering everything a professional developer needs to know to build real applications with Flask.
The “Flask or Django?” question comes up in every Python web development discussion. The answer depends entirely on what you are building and how you prefer to work.
Neither framework is objectively better. Flask gives you freedom; Django gives you structure. Senior developers often use both — Flask for APIs and services, Django for full-stack web applications. This tutorial focuses on Flask because understanding it teaches you how web frameworks actually work, which makes you better with any framework.
Before installing Flask, set up a virtual environment. If you are not familiar with virtual environments and pip, review our Python Advanced – Virtual Environments & pip tutorial first. Never install Flask globally — always isolate your project dependencies.
# Create a project directory mkdir flask-tutorial && cd flask-tutorial # Create a virtual environment python3 -m venv venv # Activate it # macOS / Linux source venv/bin/activate # Windows venv\Scripts\activate
# Install Flask pip install flask # Verify the installation python -c "import flask; print(flask.__version__)"
Flask installs its dependencies automatically: Werkzeug, Jinja2, MarkupSafe, ItsDangerous, Click, and Blinker. You do not need to install these separately.
# Save your dependencies pip freeze > requirements.txt
Your requirements.txt should look something like this:
blinker==1.9.0 click==8.1.7 Flask==3.1.0 itsdangerous==2.2.0 Jinja2==3.1.4 MarkupSafe==3.0.2 Werkzeug==3.1.3
Let us build the classic “Hello World” application. Create a file called app.py.
from flask import Flask
app = Flask(__name__)
@app.route("/")
def hello():
return "Hello, World!"
if __name__ == "__main__":
app.run(debug=True)
Let us break down every line:
from flask import Flask — Import the Flask class. An instance of this class is your WSGI application.app = Flask(__name__) — Create an application instance. The __name__ argument tells Flask where to look for templates, static files, and other resources. When you run the file directly, __name__ is "__main__". When imported as a module, it is the module name.@app.route("/") — A decorator that tells Flask which URL should trigger this function. The "/" means the root URL of your application.def hello() — The view function. Flask calls this when someone visits the root URL. Whatever it returns becomes the HTTP response body.app.run(debug=True) — Starts the built-in development server with debug mode enabled.You have two options for running your Flask app:
# Option 1: Run the file directly python app.py # Option 2: Use the flask command (recommended) flask --app app run --debug
Both methods start the development server on http://127.0.0.1:5000. Open that URL in your browser and you should see “Hello, World!”.
Debug mode provides two critical features during development:
Warning: Never enable debug mode in production. The interactive debugger allows arbitrary code execution on your server. We will cover this in more detail in the Common Pitfalls section.
Flask does not enforce a project structure, which is both its strength and a potential pitfall. Here are two structures that work well depending on your project size.
For small apps, APIs, or prototypes, a flat structure works fine:
flask-tutorial/ ├── app.py # Application code ├── templates/ # Jinja2 templates │ ├── base.html │ ├── index.html │ └── about.html ├── static/ # CSS, JS, images │ ├── css/ │ │ └── style.css │ └── js/ │ └── main.js ├── requirements.txt ├── .env ├── .gitignore └── venv/
When your application grows beyond a few hundred lines, organize it as a Python package using the application factory pattern:
flask-tutorial/ ├── app/ │ ├── __init__.py # Application factory (create_app) │ ├── routes/ │ │ ├── __init__.py │ │ ├── main.py # Main blueprint │ │ ├── auth.py # Auth blueprint │ │ └── api.py # API blueprint │ ├── models/ │ │ ├── __init__.py │ │ └── user.py │ ├── templates/ │ │ ├── base.html │ │ ├── index.html │ │ └── auth/ │ │ ├── login.html │ │ └── register.html │ ├── static/ │ │ ├── css/ │ │ └── js/ │ └── config.py ├── tests/ │ ├── __init__.py │ ├── conftest.py │ └── test_routes.py ├── requirements.txt ├── .env ├── .gitignore └── run.py
The application factory is a function that creates and configures your Flask app. It is the recommended pattern for any non-trivial Flask application because it allows you to create multiple instances with different configurations (useful for testing), avoids circular imports, and keeps your application modular.
# app/__init__.py
from flask import Flask
def create_app(config_name="default"):
app = Flask(__name__)
# Load configuration
if config_name == "testing":
app.config.from_object("app.config.TestingConfig")
elif config_name == "production":
app.config.from_object("app.config.ProductionConfig")
else:
app.config.from_object("app.config.DevelopmentConfig")
# Register blueprints
from app.routes.main import main_bp
app.register_blueprint(main_bp)
from app.routes.auth import auth_bp
app.register_blueprint(auth_bp, url_prefix="/auth")
return app
# run.py
from app import create_app
app = create_app()
if __name__ == "__main__":
app.run(debug=True)
Flask applications need configuration for things like secret keys, database URLs, debug flags, and third-party API credentials. Flask provides several ways to manage configuration, and choosing the right approach matters for both security and maintainability.
The cleanest approach is to define configuration as Python classes. Each class represents an environment.
# config.py
import os
class Config:
"""Base configuration shared by all environments."""
SECRET_KEY = os.environ.get("SECRET_KEY", "fallback-secret-key")
SQLALCHEMY_TRACK_MODIFICATIONS = False
class DevelopmentConfig(Config):
"""Development-specific configuration."""
DEBUG = True
SQLALCHEMY_DATABASE_URI = os.environ.get(
"DATABASE_URL", "sqlite:///dev.db"
)
class TestingConfig(Config):
"""Testing-specific configuration."""
TESTING = True
SQLALCHEMY_DATABASE_URI = "sqlite:///test.db"
class ProductionConfig(Config):
"""Production-specific configuration."""
DEBUG = False
SQLALCHEMY_DATABASE_URI = os.environ.get("DATABASE_URL")
# In production, SECRET_KEY must be set via environment variable
SECRET_KEY = os.environ["SECRET_KEY"]
# Load from a class
app.config.from_object("config.DevelopmentConfig")
# Load from environment variable pointing to a config file
app.config.from_envvar("APP_CONFIG_FILE")
# Set individual values
app.config["MAX_CONTENT_LENGTH"] = 16 * 1024 * 1024 # 16 MB upload limit
For local development, storing environment variables in a .env file is standard practice. Install python-dotenv and Flask will automatically load .env files.
pip install python-dotenv
# .env (add this to .gitignore!) FLASK_APP=app FLASK_DEBUG=1 SECRET_KEY=my-super-secret-key-change-in-production DATABASE_URL=sqlite:///dev.db
from dotenv import load_dotenv
load_dotenv() # Load variables from .env file
import os
secret = os.environ.get("SECRET_KEY")
Never commit .env files to version control. They contain secrets. Add .env to your .gitignore and provide a .env.example file with placeholder values so teammates know which variables to set.
Routing is the mechanism that maps URLs to Python functions. In Flask, you define routes using the @app.route() decorator.
@app.route("/")
def index():
return "Home Page"
@app.route("/about")
def about():
return "About Page"
@app.route("/contact")
def contact():
return "Contact Page"
You can capture parts of the URL as variables and pass them to your view function. Flask supports several converter types.
# String (default) - accepts any text without slashes
@app.route("/user/<username>")
def user_profile(username):
return f"Profile: {username}"
# Integer
@app.route("/post/<int:post_id>")
def show_post(post_id):
return f"Post #{post_id}"
# Float
@app.route("/price/<float:amount>")
def show_price(amount):
return f"Price: ${amount:.2f}"
# Path - like string but accepts slashes
@app.route("/files/<path:filepath>")
def serve_file(filepath):
return f"Serving: {filepath}"
# UUID
@app.route("/item/<uuid:item_id>")
def get_item(item_id):
return f"Item: {item_id}"
By default, routes only respond to GET requests. Use the methods parameter to accept other HTTP methods.
from flask import request, redirect, url_for, render_template, jsonify
@app.route("/login", methods=["GET", "POST"])
def login():
if request.method == "POST":
username = request.form["username"]
password = request.form["password"]
# Authenticate user...
return redirect(url_for("index"))
return render_template("login.html")
@app.route("/api/users", methods=["GET"])
def list_users():
users = [{"id": 1, "name": "Folau"}, {"id": 2, "name": "Sione"}]
return jsonify(users)
@app.route("/api/users", methods=["POST"])
def create_user():
data = request.get_json()
# Create user...
return jsonify({"id": 3, "name": data["name"]}), 201
@app.route("/api/users/<int:user_id>", methods=["PUT"])
def update_user(user_id):
data = request.get_json()
# Update user...
return jsonify({"id": user_id, "name": data["name"]})
@app.route("/api/users/<int:user_id>", methods=["DELETE"])
def delete_user(user_id):
# Delete user...
return "", 204
url_for() generates URLs based on function names rather than hardcoded paths. This is critical because if you change a URL pattern, all url_for() calls automatically update.
from flask import url_for
@app.route("/")
def index():
return "Home"
@app.route("/user/<username>")
def profile(username):
return f"Profile: {username}"
# In your code or templates:
with app.test_request_context():
print(url_for("index")) # /
print(url_for("profile", username="folau")) # /user/folau
print(url_for("static", filename="css/style.css")) # /static/css/style.css
Always use url_for() instead of hardcoding URLs. It handles URL encoding, respects your application’s root path, and makes refactoring painless.
Flask provides a request object that gives you access to all incoming HTTP request data, and several utilities for building responses.
from flask import request
@app.route("/search")
def search():
# Query string parameters: /search?q=flask&page=2
query = request.args.get("q", "")
page = request.args.get("page", 1, type=int)
return f"Searching for '{query}' on page {page}"
@app.route("/submit", methods=["POST"])
def submit():
# Form data (application/x-www-form-urlencoded or multipart/form-data)
name = request.form.get("name")
email = request.form.get("email")
return f"Received: {name}, {email}"
@app.route("/api/data", methods=["POST"])
def receive_json():
# JSON data (application/json)
data = request.get_json()
return jsonify({"received": data})
@app.route("/upload", methods=["POST"])
def upload_file():
# File uploads
file = request.files.get("document")
if file:
file.save(f"./uploads/{file.filename}")
return f"Uploaded: {file.filename}"
return "No file provided", 400
@app.route("/debug-request")
def debug_request():
info = {
"method": request.method, # GET, POST, etc.
"url": request.url, # Full URL
"path": request.path, # /debug-request
"host": request.host, # localhost:5000
"headers": dict(request.headers), # All headers
"content_type": request.content_type, # Request content type
"remote_addr": request.remote_addr, # Client IP address
"cookies": dict(request.cookies), # All cookies
}
return jsonify(info)
from flask import make_response, jsonify, redirect, url_for
# Simple string response (200 OK by default)
@app.route("/simple")
def simple():
return "Hello"
# String with custom status code
@app.route("/not-found-example")
def custom_status():
return "This page does not exist", 404
# JSON response
@app.route("/api/status")
def api_status():
return jsonify({"status": "healthy", "version": "1.0.0"})
# Full control with make_response
@app.route("/custom")
def custom_response():
resp = make_response("Custom Response", 200)
resp.headers["X-Custom-Header"] = "MyValue"
resp.set_cookie("visited", "true", max_age=3600)
return resp
# Redirect
@app.route("/old-page")
def old_page():
return redirect(url_for("index"))
Returning HTML strings from view functions is impractical for real applications. Flask uses the Jinja2 template engine to separate your presentation logic from your application logic. Templates live in a templates/ directory by default.
from flask import render_template
@app.route("/")
def index():
return render_template("index.html", title="Home", user="Folau")
@app.route("/posts")
def posts():
all_posts = [
{"id": 1, "title": "Flask Basics", "author": "Folau"},
{"id": 2, "title": "Jinja2 Templates", "author": "Folau"},
{"id": 3, "title": "Flask Routing", "author": "Folau"},
]
return render_template("posts.html", posts=all_posts)
Template inheritance is one of Jinja2’s most powerful features. You define a base template with blocks that child templates can override. This eliminates HTML duplication across pages.
<!-- templates/base.html -->
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>{% block title %}My Flask App{% endblock %}</title>
<link rel="stylesheet" href="{{ url_for('static', filename='css/style.css') }}">
</head>
<body>
<nav>
<a href="{{ url_for('index') }}">Home</a>
<a href="{{ url_for('about') }}">About</a>
<a href="{{ url_for('posts') }}">Posts</a>
</nav>
<main>
{% block content %}{% endblock %}
</main>
<footer>
<p>© 2026 My Flask App</p>
</footer>
</body>
</html>
<!-- templates/index.html -->
{% extends "base.html" %}
{% block title %}Home - My Flask App{% endblock %}
{% block content %}
<h1>Welcome, {{ user }}!</h1>
<p>This is a Flask application with Jinja2 templates.</p>
{% endblock %}
<!-- Output a variable -->
<h1>{{ title }}</h1>
<!-- Access object attributes -->
<p>{{ user.name }}</p>
<!-- Access dictionary keys -->
<p>{{ config['SECRET_KEY'] }}</p>
<!-- Expressions -->
<p>{{ items | length }} items in your cart</p>
<!-- Conditionals -->
{% if user %}
<p>Hello, {{ user.name }}!</p>
{% elif guest %}
<p>Hello, guest!</p>
{% else %}
<p>Please log in.</p>
{% endif %}
<!-- Loops -->
<ul>
{% for post in posts %}
<li>
<a href="{{ url_for('show_post', post_id=post.id) }}">
{{ post.title }}
</a>
<span>by {{ post.author }}</span>
</li>
{% else %}
<li>No posts found.</li>
{% endfor %}
</ul>
<!-- Loop with index -->
{% for item in items %}
<p>{{ loop.index }}. {{ item }}</p>
{% endfor %}
Jinja2 filters transform values. They are applied using the pipe | operator.
<!-- Built-in filters -->
{{ name | capitalize }}
{{ name | upper }}
{{ name | lower }}
{{ description | truncate(100) }}
{{ items | length }}
{{ price | round(2) }}
{{ html_content | safe }} <!-- Render HTML without escaping -->
{{ list | join(", ") }}
{{ date | default("No date") }}
By default, Jinja2 auto-escapes HTML in variables to prevent XSS attacks. The | safe filter tells Jinja2 you trust the content and it should be rendered as raw HTML. Use this sparingly and only with content you control.
Static files — CSS, JavaScript, images — live in the static/ directory. Flask automatically serves this directory at the /static URL path.
static/
├── css/
│ ├── style.css
│ └── bootstrap.min.css
├── js/
│ ├── main.js
│ └── jquery.min.js
└── images/
├── logo.png
└── favicon.ico
Always use url_for('static', filename=...) to reference static files. Never hardcode paths.
<!-- CSS -->
<link rel="stylesheet" href="{{ url_for('static', filename='css/style.css') }}">
<!-- JavaScript -->
<script src="{{ url_for('static', filename='js/main.js') }}"></script>
<!-- Images -->
<img src="{{ url_for('static', filename='images/logo.png') }}" alt="Logo">
<!-- Favicon -->
<link rel="icon" href="{{ url_for('static', filename='images/favicon.ico') }}">
Using url_for() ensures correct paths regardless of where your application is mounted. If your app runs at /myapp/ instead of /, url_for() adjusts automatically. Hardcoded paths would break.
Flask lets you register custom error handlers so your users see friendly, branded error pages instead of generic browser defaults.
from flask import render_template
@app.errorhandler(404)
def page_not_found(error):
return render_template("errors/404.html"), 404
@app.errorhandler(500)
def internal_server_error(error):
return render_template("errors/500.html"), 500
@app.errorhandler(403)
def forbidden(error):
return render_template("errors/403.html"), 403
<!-- templates/errors/404.html -->
{% extends "base.html" %}
{% block title %}Page Not Found{% endblock %}
{% block content %}
<div class="error-page">
<h1>404</h1>
<h2>Page Not Found</h2>
<p>The page you are looking for does not exist.</p>
<a href="{{ url_for('index') }}">Go back home</a>
</div>
{% endblock %}
For JSON APIs, return JSON error responses instead of HTML.
from flask import jsonify
from werkzeug.exceptions import HTTPException
@app.errorhandler(HTTPException)
def handle_http_exception(error):
response = jsonify({
"error": error.name,
"message": error.description,
"status_code": error.code,
})
response.status_code = error.code
return response
@app.errorhandler(Exception)
def handle_generic_exception(error):
# Log the error for debugging
app.logger.error(f"Unhandled exception: {error}", exc_info=True)
response = jsonify({
"error": "Internal Server Error",
"message": "An unexpected error occurred.",
"status_code": 500,
})
response.status_code = 500
return response
Use abort() to immediately stop a request and return an error status code.
from flask import abort
@app.route("/admin")
def admin_panel():
if not current_user.is_admin:
abort(403) # Forbidden
return render_template("admin.html")
@app.route("/api/users/<int:user_id>")
def get_user(user_id):
user = User.query.get(user_id)
if user is None:
abort(404) # Not Found
return jsonify(user.to_dict())
Let us pull everything together and build a simple blog application with multiple routes, template inheritance, and static CSS. This example demonstrates real-world Flask patterns you will use in production applications.
# app.py
from flask import Flask, render_template, abort
app = Flask(__name__)
# Simulated blog data (in a real app, this comes from a database)
POSTS = [
{
"id": 1,
"title": "Getting Started with Flask",
"slug": "getting-started-with-flask",
"author": "Folau Kaveinga",
"date": "2026-02-20",
"summary": "Learn the basics of Flask, the lightweight Python web framework.",
"content": (
"Flask is a micro-framework for Python that gives you the essentials "
"for building web applications without imposing unnecessary complexity. "
"In this post, we explore the core concepts that make Flask a favorite "
"among Python developers."
),
},
{
"id": 2,
"title": "Understanding Jinja2 Templates",
"slug": "understanding-jinja2-templates",
"author": "Folau Kaveinga",
"date": "2026-02-22",
"summary": "Deep dive into Jinja2 template engine features and best practices.",
"content": (
"Jinja2 is the template engine that powers Flask's HTML rendering. "
"It supports template inheritance, filters, macros, and more. "
"Mastering Jinja2 is essential for building maintainable Flask applications."
),
},
{
"id": 3,
"title": "Flask Routing and URL Building",
"slug": "flask-routing-and-url-building",
"author": "Folau Kaveinga",
"date": "2026-02-25",
"summary": "Master Flask routing, variable rules, and URL generation.",
"content": (
"Routing is the backbone of any web application. Flask makes it "
"intuitive with decorators and supports variable rules, multiple "
"HTTP methods, and dynamic URL building with url_for()."
),
},
]
@app.route("/")
def index():
return render_template("blog/index.html", posts=POSTS)
@app.route("/about")
def about():
return render_template("blog/about.html")
@app.route("/post/<int:post_id>")
def post_detail(post_id):
post = next((p for p in POSTS if p["id"] == post_id), None)
if post is None:
abort(404)
return render_template("blog/post.html", post=post)
@app.errorhandler(404)
def page_not_found(error):
return render_template("blog/404.html"), 404
if __name__ == "__main__":
app.run(debug=True)
<!-- templates/blog/base.html -->
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>{% block title %}Flask Blog{% endblock %}</title>
<link rel="stylesheet" href="{{ url_for('static', filename='css/blog.css') }}">
</head>
<body>
<header>
<nav>
<div class="nav-brand">Flask Blog</div>
<div class="nav-links">
<a href="{{ url_for('index') }}">Home</a>
<a href="{{ url_for('about') }}">About</a>
</div>
</nav>
</header>
<main>
{% block content %}{% endblock %}
</main>
<footer>
<p>Built with Flask — © 2026</p>
</footer>
</body>
</html>
<!-- templates/blog/index.html -->
{% extends "blog/base.html" %}
{% block title %}Home - Flask Blog{% endblock %}
{% block content %}
<h1>Latest Posts</h1>
{% for post in posts %}
<article class="post-card">
<h2>
<a href="{{ url_for('post_detail', post_id=post.id) }}">{{ post.title }}</a>
</h2>
<div class="post-meta">{{ post.author }} · {{ post.date }}</div>
<p>{{ post.summary }}</p>
<a href="{{ url_for('post_detail', post_id=post.id) }}" class="read-more">
Read more →
</a>
</article>
{% endfor %}
{% endblock %}
<!-- templates/blog/post.html -->
{% extends "blog/base.html" %}
{% block title %}{{ post.title }} - Flask Blog{% endblock %}
{% block content %}
<article class="post-full">
<h1>{{ post.title }}</h1>
<div class="post-meta">{{ post.author }} · {{ post.date }}</div>
<div class="post-content">
{{ post.content }}
</div>
<a href="{{ url_for('index') }}">← Back to all posts</a>
</article>
{% endblock %}
<!-- templates/blog/about.html -->
{% extends "blog/base.html" %}
{% block title %}About - Flask Blog{% endblock %}
{% block content %}
<h1>About This Blog</h1>
<p>This is a simple blog built with Flask to demonstrate core framework concepts
including routing, template inheritance, static files, and error handling.</p>
<p>Flask makes it straightforward to build web applications in Python without
the overhead of a full-stack framework. You choose the components you need
and assemble them yourself.</p>
{% endblock %}
/* static/css/blog.css */
* {
margin: 0;
padding: 0;
box-sizing: border-box;
}
body {
font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, sans-serif;
line-height: 1.6;
color: #333;
max-width: 800px;
margin: 0 auto;
padding: 0 20px;
}
header nav {
display: flex;
justify-content: space-between;
align-items: center;
padding: 20px 0;
border-bottom: 2px solid #eee;
}
.nav-brand {
font-size: 1.5rem;
font-weight: bold;
color: #2563eb;
}
.nav-links a {
margin-left: 20px;
text-decoration: none;
color: #555;
}
.nav-links a:hover { color: #2563eb; }
main { padding: 40px 0; }
.post-card {
margin-bottom: 30px;
padding-bottom: 30px;
border-bottom: 1px solid #eee;
}
.post-card h2 a {
text-decoration: none;
color: #1a1a1a;
}
.post-card h2 a:hover { color: #2563eb; }
.post-meta {
color: #888;
font-size: 0.9rem;
margin: 8px 0;
}
.read-more {
color: #2563eb;
text-decoration: none;
font-weight: 500;
}
.post-full h1 { margin-bottom: 10px; }
.post-content {
margin: 20px 0;
line-height: 1.8;
}
footer {
padding: 20px 0;
border-top: 2px solid #eee;
text-align: center;
color: #888;
}
Run the application with python app.py and visit http://127.0.0.1:5000. You now have a working blog with a home page listing posts, individual post pages, an about page, and custom 404 handling. All of it using template inheritance so the navigation and footer are defined once in base.html.
The Flask development server is single-threaded, unoptimized, and not designed to handle real traffic. Never use it in production. Instead, use a production-grade WSGI server like Gunicorn.
# Install gunicorn pip install gunicorn # Run your Flask app with gunicorn gunicorn app:app --bind 0.0.0.0:8000 --workers 4 # With the application factory pattern gunicorn "app:create_app()" --bind 0.0.0.0:8000 --workers 4
The --workers 4 flag spawns 4 worker processes to handle requests concurrently. A common rule of thumb is (2 * CPU cores) + 1 workers.
In production, Flask applications typically run behind a reverse proxy:
Client → Nginx (reverse proxy, SSL, static files) → Gunicorn (WSGI server) → Flask (application)
Nginx handles SSL termination, static file serving, and load balancing. Gunicorn runs your Flask application. This separation of concerns gives you better performance, security, and reliability than any single component could provide alone.
This is the most dangerous mistake you can make with Flask. The interactive debugger that runs in debug mode allows anyone who can reach your error page to execute arbitrary Python code on your server. This means full remote code execution — they can read your environment variables, access your database, and compromise your entire system.
# NEVER do this in production
app.run(debug=True)
# Instead, control debug mode via environment variables
import os
app.run(debug=os.environ.get("FLASK_DEBUG", "0") == "1")
This is the most common structural issue in Flask applications. It happens when your application module and your routes module try to import each other.
# BAD: Circular import
# app.py
from flask import Flask
app = Flask(__name__)
from routes import * # routes.py imports app from here = circular
# GOOD: Use the application factory pattern
# app/__init__.py
def create_app():
app = Flask(__name__)
from app.routes import main_bp
app.register_blueprint(main_bp)
return app
The application factory pattern solves circular imports because the app object is created inside a function, not at module level. Blueprints do not need to import the app object directly.
Hardcoding database URLs, secret keys, and API keys directly in your source code is a security risk and makes deployment difficult.
# BAD app.config["SECRET_KEY"] = "my-secret-key" app.config["DATABASE_URL"] = "postgresql://user:pass@localhost/db" # GOOD app.config["SECRET_KEY"] = os.environ["SECRET_KEY"] app.config["DATABASE_URL"] = os.environ["DATABASE_URL"]
Hardcoding URLs in templates and Python code breaks when you change route paths or deploy your app under a different prefix.
<!-- BAD -->
<a href="/about">About</a>
<link rel="stylesheet" href="/static/css/style.css">
<!-- GOOD -->
<a href="{{ url_for('about') }}">About</a>
<link rel="stylesheet" href="{{ url_for('static', filename='css/style.css') }}">
Some Flask operations require an application context. If you see “RuntimeError: Working outside of application context,” it means you are trying to use Flask features (like url_for() or current_app) outside of a request or without an active app context.
# If you need app context outside a request (e.g., in a script or test)
with app.app_context():
url = url_for("index")
db.create_all()
app object into a factory later is painful. Start with create_app() and save yourself the trouble.app object because it makes the transition to multiple blueprints trivial.python-dotenv. Your .env file should be in .gitignore.logging module. Configure it to write to files or a centralized logging service in production. app.logger is your starting point.import logging
from logging.handlers import RotatingFileHandler
if not app.debug:
handler = RotatingFileHandler("app.log", maxBytes=10240, backupCount=10)
handler.setLevel(logging.INFO)
formatter = logging.Formatter(
"%(asctime)s %(levelname)s: %(message)s [in %(pathname)s:%(lineno)d]"
)
handler.setFormatter(formatter)
app.logger.addHandler(handler)
app.logger.setLevel(logging.INFO)
app.logger.info("Application startup")
import pytest
from app import create_app
@pytest.fixture
def client():
app = create_app("testing")
with app.test_client() as client:
yield client
def test_index(client):
response = client.get("/")
assert response.status_code == 200
def test_404(client):
response = client.get("/nonexistent")
assert response.status_code == 404
pip freeze > requirements.txt so every environment runs the same package versions.pip install flask and freeze your dependencies with pip freeze > requirements.txt.flask --app app run --debug to start the development server with auto-reloading and an interactive debugger.create_app()) for any non-trivial application. It prevents circular imports and enables testing with different configurations.python-dotenv. Never hardcode secrets in source code.@app.route(). Use variable rules for dynamic URLs and url_for() to generate URLs programmatically.request object provides access to query parameters, form data, JSON payloads, and file uploads. Use jsonify() and make_response() to build responses.url_for() in templates to reference routes and static files.@app.errorhandler() to show branded error pages instead of browser defaults.