Build a Simple Website Using Flask: The Complete Beginner’s Guide (2026)
You have learned Python basics. You can write functions, handle loops, and maybe even work with files. But now you are asking the real question: “How do I turn my Python knowledge into an actual website that people can visit?”
That is exactly where Flask comes in — and it is one of the most beginner-friendly answers to that question in the Python ecosystem.
Flask is a lightweight Python web framework that lets you go from a blank Python file to a running website in under ten minutes. No complicated setup. No overwhelming configuration. Just clean, readable Python code that makes the web work for you.
By the time you finish this guide, you will have:
- A solid understanding of what Flask is and how it works
- A fully configured Flask development environment
- The skills to build dynamic web pages using Jinja2 templates
- A working contact form that handles real user input
- A complete mini project: a personal website with three pages and a contact form
Whether you want to build a portfolio, a tool for your team, or just understand how web applications work, this is the right place to start. Let’s build something real.
What Is Flask? (And Why Not Django?)
Before writing a single line of code, it helps to understand what Flask actually is — and why so many developers, from total beginners to professional engineers, reach for it first.
Flask: The Lean, Mean Web Machine
Flask is a micro web framework for Python. The word “micro” here does not mean it is weak or limited. It means Flask is lean — it gives you exactly the tools you need to build a web application without forcing a dozen extra things on you.
At its core, Flask is built on two powerful libraries:
- Werkzeug — handles HTTP requests and responses (the plumbing of the web)
- Jinja2 — renders dynamic HTML templates using Python-like syntax
That is really all Flask needs to do its job. When you want more — a database, user authentication, email sending — you simply add an extension. Flask never forces a direction on you.
Here is a real-world perspective: companies like Pinterest, Reddit, and LinkedIn have used Flask to power parts of their infrastructure. It is not just a teaching toy — it is production-grade software used at scale.
Flask vs Django — Which Should a Beginner Learn?
This is one of the most common questions new Python web developers ask. Here is an honest, practical comparison:
| Feature | Flask | Django |
|---|---|---|
| Type | Micro-framework | Full-stack framework |
| Learning Curve | Low — beginner-friendly | Steeper — more concepts upfront |
| Project Structure | You decide | Follows strict conventions |
| Built-in Features | Minimal (add what you need) | Built-in ORM, admin, auth |
| Best For | Learning, APIs, small-to-mid apps | Large platforms, content sites |
| Flexibility | Very high | More opinionated |
For beginners, Flask wins. It lets you learn web development concepts one at a time without overwhelming you with Django’s “batteries included” complexity. Once you understand how Flask works, picking up Django later becomes much easier — because you already understand the underlying concepts.
Installing Flask and Setting Up Your Environment
A good development environment is the foundation of every successful project. This section walks you through a clean, modern setup that follows current best practices as of 2026.
Step 1 — Check Your Python Version
Flask requires Python 3.7 or higher. Flask officially dropped support for Python 2 starting with version 2.0, so any modern Python 3 installation will work. For best performance and compatibility with current extensions, Python 3.11 or later is strongly recommended.
Open your terminal or command prompt and run:
python --version
# or on some systems:
python3 --version
If you see Python 3.11.x or higher, you are good to go. If not, download the latest stable version from the official Python website.
Step 2 — Create a Project Folder
Keep things organized from the start. Create a dedicated folder for your Flask project:
mkdir my_flask_site
cd my_flask_site
Step 3 — Set Up a Virtual Environment
This step is one that beginners often skip — and later regret. A virtual environment creates an isolated Python workspace specifically for your project. This prevents package conflicts between different projects on your machine.
# Create the virtual environment
python -m venv venv
# Activate it — Mac/Linux:
source venv/bin/activate
# Activate it — Windows:
venv\Scripts\activate
When your virtual environment is active, you will see (venv) at the beginning of your terminal prompt. That tells you all packages you install will be scoped to this project only.
💡 Pro Tip: Never skip the virtual environment step. It is a habit that separates hobbyists from professional developers.
Step 4 — Install Flask
With your virtual environment active, install Flask using pip:
pip install flask
This single command installs Flask along with its two core dependencies: Werkzeug (HTTP handling) and Jinja2 (templating). Verify the installation:
flask --version
You should see output similar to:
Python 3.11.x
Flask 3.1.x
Werkzeug 3.x.x
Step 5 — Save Your Dependencies
Always record your project’s dependencies. This allows anyone (including future you) to recreate the environment easily:
pip freeze > requirements.txt
To reinstall everything later on a new machine:
pip install -r requirements.txt
Your project folder now looks like this:
my_flask_site/
├── venv/
└── requirements.txt
You are ready to write your first Flask application.
Creating Your First Flask App
Let us write the simplest possible Flask application and get it running in a browser. This will prove your environment is configured correctly and introduce the fundamental building blocks of every Flask app.
The Minimal Flask Application
Create a new file called app.py in your project folder:
from flask import Flask
# Create the Flask application instance
app = Flask(__name__)
# Define your first route
@app.route('/')
def home():
return "Hello, World! Welcome to my Flask website."
# Run the app when this file is executed directly
if __name__ == '__main__':
app.run(debug=True)
That is a complete, working web application in just 10 lines of Python. Let us break down what each part does:
| Line | Explanation |
|---|---|
from flask import Flask | Imports the Flask class from the flask package |
app = Flask(__name__) | Creates your Flask app instance; __name__ tells Flask where to find resources |
@app.route('/') | A decorator that maps the URL / to the function below it |
def home(): | The view function that runs when someone visits / |
return "Hello, World!" | The HTTP response sent back to the browser |
app.run(debug=True) | Starts the development server with auto-reload enabled |
Running Your Flask App
In your terminal (with the virtual environment active), run:
flask run
You will see output like this:
* Running on http://127.0.0.1:5000
* Debug mode: on
Open your browser and visit http://127.0.0.1:5000. You should see:
Hello, World! Welcome to my Flask website.
Congratulations — you just built and ran your first Flask web application.
⚠️ Important:
debug=Trueis only for development. It enables detailed error messages and automatic server restarts when you save changes. Never usedebug=Truein a production environment — it exposes sensitive server information to the public.
Understanding Routing in Flask
Routing is the system that connects URLs to Python functions. When a user visits yoursite.com/about, Flask needs to know which Python function to call. That mapping is what routing handles.
Basic Routes
You can define as many routes as your site needs:
from flask import Flask
app = Flask(__name__)
@app.route('/')
def home():
return "Welcome to the Home Page"
@app.route('/about')
def about():
return "This is the About Page"
@app.route('/contact')
def contact():
return "This is the Contact Page"
Each @app.route() decorator connects a URL path to a view function. The function name does not have to match the URL, but it is good practice to keep them descriptive and consistent.
Dynamic URL Parameters
Flask lets you capture parts of the URL as variables. This is how you build dynamic pages — like user profiles or blog post pages:
@app.route('/user/<username>')
def profile(username):
return f"Hello, {username}! This is your profile page."
@app.route('/post/<int:post_id>')
def show_post(post_id):
return f"Showing post number: {post_id}"
The <username> syntax captures any string from the URL. The <int:post_id> syntax captures an integer. Visit http://127.0.0.1:5000/user/Sarah and Flask will pass "Sarah" as the username argument to your function.
HTTP Methods — GET and POST
Every web request has an HTTP method. The two most common are:
- GET — Used to retrieve data (loading a page, clicking a link)
- POST — Used to submit data (submitting a form)
By default, Flask routes only accept GET requests. To accept POST requests (for forms), specify the methods parameter:
from flask import Flask, request
app = Flask(__name__)
@app.route('/submit', methods=['GET', 'POST'])
def submit():
if request.method == 'POST':
name = request.form['name']
return f"Thank you, {name}! We received your submission."
return "Please fill out the form."
Using url_for() for Safe URL Generation
Hardcoding URL strings in templates is fragile. If you ever change a route, every link to it breaks. The url_for() function solves this by generating URLs from function names instead of strings:
from flask import url_for
# Instead of: href="/about"
# Use: href="{{ url_for('about') }}"
This way, if you change your /about route to /about-us, your links still work automatically. Always use url_for() in your templates — it is a Flask best practice that pays dividends as your project grows.
Templates with Jinja2
Returning raw strings from view functions works for testing, but real websites need HTML. Flask uses the Jinja2 template engine to render proper HTML pages with dynamic content.
What Is a Template?
A template is an HTML file with special placeholder syntax that Jinja2 processes before sending the page to the browser. Think of it like a form letter — the structure stays the same, but specific values get filled in dynamically based on data from your Python code.
Setting Up the Templates Folder
Flask automatically looks for templates in a folder named templates/. Create it now:
my_flask_site/
├── app.py
├── requirements.txt
├── templates/
│ └── index.html
└── venv/
Your First Template
Create templates/index.html:
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>{{ page_title }}</title>
</head>
<body>
<h1>Welcome, {{ username }}!</h1>
<p>This is your Flask-powered website.</p>
</body>
</html>
Notice the {{ page_title }} and {{ username }} — these are Jinja2 variables that get replaced with real values when the template is rendered.
Rendering Templates in Flask
Update app.py to render this template:
from flask import Flask, render_template
app = Flask(__name__)
@app.route('/')
def home():
return render_template('index.html', page_title='My Flask Site', username='Sarah')
The render_template() function loads the HTML file, passes your Python variables into it via keyword arguments, and returns the final rendered HTML to the browser.
Jinja2 Syntax Quick Reference
Jinja2 uses three types of delimiters:
| Delimiter | Purpose | Example |
|---|---|---|
{{ }} | Output a variable | {{ username }} |
{% %} | Control logic (if, for, block) | {% if user %} |
{# #} | Comments (not shown in HTML) | {# This is a comment #} |
Variables:
<h1>Hello, {{ name }}!</h1>
<p>You have {{ message_count }} new messages.</p>
Conditional Statements:
{% if user_logged_in %}
<p>Welcome back, {{ username }}!</p>
{% else %}
<p>Please <a href="/login">log in</a> to continue.</p>
{% endif %}
For Loops:
<ul>
{% for item in shopping_list %}
<li>{{ item }}</li>
{% endfor %}
</ul>
Template Inheritance — The Base Layout Pattern
One of Jinja2’s most powerful features is template inheritance. Instead of copying the same <head>, navigation, and footer HTML into every page, you create one base layout and extend it across all your pages.
Create 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 Site{% endblock %}</title>
<link rel="stylesheet" href="{{ url_for('static', filename='css/style.css') }}">
</head>
<body>
<!-- Navigation Bar -->
<nav>
<a href="{{ url_for('home') }}">Home</a>
<a href="{{ url_for('about') }}">About</a>
<a href="{{ url_for('contact') }}">Contact</a>
</nav>
<!-- Main Content Block -->
<main>
{% block content %}
{% endblock %}
</main>
<!-- Footer -->
<footer>
<p>© 2026 My Flask Website. All rights reserved.</p>
</footer>
</body>
</html>
Now create templates/index.html that extends this base:
{% extends "base.html" %}
{% block title %}Home — My Flask Site{% endblock %}
{% block content %}
<h1>Welcome to My Website</h1>
<p>This site is built with Python and Flask.</p>
{% endblock %}
With template inheritance, your navigation and footer live in one place. If you need to update them, you change one file — not twenty.
Security Note — Jinja2 Auto-Escaping
Jinja2 automatically escapes HTML characters in variables to prevent XSS (Cross-Site Scripting) attacks. This means if a user submits <script>alert('hacked')</script> into a form, Jinja2 will display it as text — not execute it as code.
<!-- Safe — Jinja2 escapes this automatically -->
<p>{{ user_comment }}</p>
<!-- DANGER — Only use | safe on content YOU fully control -->
<p>{{ trusted_html_content | safe }}</p>
Never use | safe on user-submitted content. This is a critical security rule.
Handling Forms and User Input
Forms are how websites collect information from users. In Flask, handling forms involves two steps: displaying the form (GET request) and processing its data (POST request).
Before diving into web forms, make sure you are comfortable with how Python itself handles input at the language level. If you need a refresher, this guide on how to take user input in Python covers the fundamentals that underpin everything Flask does with user data.
Step 1 — Create the Form Template
Create templates/contact.html:
{% extends "base.html" %}
{% block title %}Contact — My Flask Site{% endblock %}
{% block content %}
<h1>Get In Touch</h1>
{% if success %}
<div class="success-message">
<p>✅ Thank you, {{ submitted_name }}! Your message has been received.</p>
</div>
{% endif %}
<form action="/contact" method="POST">
<div class="form-group">
<label for="name">Your Name:</label>
<input type="text" id="name" name="name" required placeholder="Jane Doe">
</div>
<div class="form-group">
<label for="email">Your Email:</label>
<input type="email" id="email" name="email" required placeholder="jane@example.com">
</div>
<div class="form-group">
<label for="message">Your Message:</label>
<textarea id="message" name="message" rows="5" required placeholder="Write your message here..."></textarea>
</div>
<button type="submit">Send Message</button>
</form>
{% endblock %}
Step 2 — Process the Form in Flask
Update app.py to handle the contact form:
from flask import Flask, render_template, request
app = Flask(__name__)
@app.route('/contact', methods=['GET', 'POST'])
def contact():
success = False
submitted_name = ''
if request.method == 'POST':
name = request.form.get('name', '').strip()
email = request.form.get('email', '').strip()
message = request.form.get('message', '').strip()
# Basic validation
if name and email and message:
success = True
submitted_name = name
# Here you could save to a file, database, or send an email
return render_template('contact.html', success=success, submitted_name=submitted_name)
Understanding request.form
When a user submits a form, Flask stores all form data in request.form — a dictionary-like object. Use .get('field_name', '') instead of direct access ['field_name'] to safely handle cases where a field might be missing.
Using Flask-WTF for Production Forms
For any serious project, the Flask-WTF extension adds essential features: CSRF protection, built-in validation, and cleaner code:
pip install flask-wtf
from flask_wtf import FlaskForm
from wtforms import StringField, TextAreaField, SubmitField
from wtforms.validators import DataRequired, Email
class ContactForm(FlaskForm):
name = StringField('Name', validators=[DataRequired()])
email = StringField('Email', validators=[DataRequired(), Email()])
message = TextAreaField('Message', validators=[DataRequired()])
submit = SubmitField('Send Message')
Flask-WTF automatically adds a CSRF token to every form, protecting your users from cross-site request forgery attacks. Always set a strong SECRET_KEY when using Flask-WTF:
app.config['SECRET_KEY'] = 'your-strong-random-secret-key-here'
Working with Static Files
A website without styling looks unfinished. Flask serves CSS, JavaScript, and image files from a dedicated folder called static/.
Setting Up the Static Folder
Create this structure inside your project:
my_flask_site/
├── app.py
├── requirements.txt
├── static/
│ ├── css/
│ │ └── style.css
│ ├── js/
│ │ └── script.js
│ └── images/
│ └── logo.png
├── templates/
│ ├── base.html
│ ├── index.html
│ ├── about.html
│ └── contact.html
└── venv/
Linking Static Files in Templates
Always use url_for('static', filename='...') to reference static files. Never hardcode paths like /static/css/style.css — if your app is deployed at a subpath, hardcoded paths will break:
<!-- In base.html <head> section -->
<!-- CSS Stylesheet -->
<link rel="stylesheet" href="{{ url_for('static', filename='css/style.css') }}">
<!-- Favicon -->
<link rel="icon" href="{{ url_for('static', filename='images/favicon.ico') }}">
<!-- JavaScript file (usually before </body>) -->
<script src="{{ url_for('static', filename='js/script.js') }}"></script>
Adding Basic CSS Styling
Create static/css/style.css with clean, readable styles:
/* Reset & Base Styles */
* {
margin: 0;
padding: 0;
box-sizing: border-box;
}
body {
font-family: 'Segoe UI', Tahoma, Geneva, Verdana, sans-serif;
background-color: #f8f9fa;
color: #333;
line-height: 1.6;
}
/* Navigation */
nav {
background-color: #2c3e50;
padding: 1rem 2rem;
display: flex;
gap: 1.5rem;
}
nav a {
color: #fff;
text-decoration: none;
font-weight: 500;
transition: color 0.2s;
}
nav a:hover {
color: #3498db;
}
/* Main Content */
main {
max-width: 800px;
margin: 2rem auto;
padding: 0 1rem;
}
h1 {
color: #2c3e50;
margin-bottom: 1rem;
}
/* Form Styles */
.form-group {
margin-bottom: 1.2rem;
}
label {
display: block;
font-weight: 600;
margin-bottom: 0.4rem;
}
input, textarea {
width: 100%;
padding: 0.6rem;
border: 1px solid #ccc;
border-radius: 4px;
font-size: 1rem;
}
button {
background-color: #3498db;
color: white;
padding: 0.7rem 1.5rem;
border: none;
border-radius: 4px;
font-size: 1rem;
cursor: pointer;
}
button:hover {
background-color: #2980b9;
}
/* Success Message */
.success-message {
background-color: #d4edda;
color: #155724;
padding: 1rem;
border-radius: 4px;
margin-bottom: 1.5rem;
}
/* Footer */
footer {
text-align: center;
padding: 2rem;
background-color: #2c3e50;
color: #aaa;
margin-top: 3rem;
}
Serving Images
Place image files in static/images/ and reference them in templates:
<img src="{{ url_for('static', filename='images/profile-photo.jpg') }}"
alt="Profile photo of Jane Doe"
width="200">
Flask Project Structure Best Practices for Beginners
Flask gives you complete freedom in how you organize your project. That freedom is a strength — but it can also lead to messy, hard-to-maintain code if you have no guiding structure.
The Beginner Structure (Single File)
For small projects with fewer than 200–300 lines of code, a single-file approach works perfectly:
my_flask_site/
├── app.py ← All routes, configuration, and logic
├── requirements.txt ← Package dependencies
├── static/
│ ├── css/
│ │ └── style.css
│ └── images/
└── templates/
├── base.html
├── index.html
├── about.html
└── contact.html
This structure is clean, easy to understand, and completely manageable for beginner projects.
The Intermediate Structure (Package Layout)
When your project grows beyond a single file, it is time to split things up into a Python package. This is not necessary for your first project, but good to know for your future:
my_flask_site/
├── app/
│ ├── __init__.py ← Creates the Flask app instance
│ ├── routes.py ← All your @app.route() definitions
│ ├── forms.py ← Flask-WTF form classes
│ ├── templates/
│ └── static/
├── config.py ← App configuration (SECRET_KEY, DEBUG, etc.)
├── run.py ← Entry point to start the app
└── requirements.txt
Key Rules for a Clean Structure
1. Keep your templates/ folder inside your app directory. Flask looks there by default.
2. Keep your static/ folder organized. Subdirectories like css/, js/, and images/ prevent clutter.
3. Use a requirements.txt file. This is not optional — it is professional practice.
4. Never commit your venv/ folder to version control. Add it to .gitignore:
venv/
__pycache__/
*.pyc
.env
5. Store sensitive data in environment variables, not in your code. Your SECRET_KEY and any API keys should never be hardcoded in app.py.
Running and Testing Your Flask App
Running in Development Mode
The recommended way to run Flask in development (using Flask 3.x):
# Basic run
flask run
# With debug mode (auto-reload + detailed errors)
flask --debug run
# On a specific port
flask run --port=8080
When debug mode is on, Flask watches your files for changes. Every time you save app.py or any template, the server restarts automatically — no need to stop and restart manually.
Testing Your Routes
Open your browser and visit each route:
http://127.0.0.1:5000/— Home pagehttp://127.0.0.1:5000/about— About pagehttp://127.0.0.1:5000/contact— Contact form
Reading Flask’s terminal output is an important skill. When you make a request, Flask logs it:
127.0.0.1 - - [25/Apr/2026 10:30:45] "GET / HTTP/1.1" 200 -
127.0.0.1 - - [25/Apr/2026 10:30:52] "POST /contact HTTP/1.1" 200 -
The 200 means success. 404 means route not found. 500 means a server error occurred.
Basic Testing with pytest
Flask has a built-in test client that lets you simulate HTTP requests without a browser. This is essential for catching bugs before they reach users.
Install pytest:
pip install pytest
Create a test_app.py file:
import pytest
from app import app
@pytest.fixture
def client():
app.config['TESTING'] = True
with app.test_client() as client:
yield client
def test_home_page(client):
response = client.get('/')
assert response.status_code == 200
def test_about_page(client):
response = client.get('/about')
assert response.status_code == 200
def test_contact_form_get(client):
response = client.get('/contact')
assert response.status_code == 200
def test_contact_form_post(client):
response = client.post('/contact', data={
'name': 'Test User',
'email': 'test@example.com',
'message': 'This is a test message'
})
assert response.status_code == 200
Run your tests:
pytest test_app.py -v
Writing even a handful of tests saves you hours of manual clicking and dramatically increases your confidence when making changes.
Common Mistakes Beginners Make in Flask (And How to Avoid Them)
Every beginner makes these mistakes. Knowing them in advance saves you hours of frustration.
Mistake 1 — Skipping the Virtual Environment
The problem: Installing Flask globally pollutes your system Python and causes version conflicts between projects.
The fix: Always create and activate a virtual environment before installing anything:
python -m venv venv
source venv/bin/activate # Mac/Linux
Mistake 2 — Leaving debug=True in Production
The problem: Debug mode exposes your server’s internal state, file paths, and code to anyone who triggers an error. This is a serious security vulnerability.
The fix: Use debug mode only during development. In production, set:
app.run(debug=False)
# Or better: use environment variables
Mistake 3 — Hardcoding URLs Instead of Using url_for()
The problem: Writing href="/about" in templates. If you ever rename the route, every link breaks.
The fix:
<!-- Wrong -->
<a href="/about">About</a>
<!-- Correct -->
<a href="{{ url_for('about') }}">About</a>
Mistake 4 — No CSRF Protection on Forms
The problem: Without CSRF tokens, your forms are vulnerable to cross-site request forgery attacks where malicious websites can submit forms on behalf of your users.
The fix: Use Flask-WTF for all forms. It automatically handles CSRF protection. Set a proper SECRET_KEY in your app configuration.
Mistake 5 — Putting Everything in One Giant app.py
The problem: A 1,000-line app.py file with routes, forms, configuration, and business logic all mixed together becomes impossible to navigate and debug.
The fix: Organize your code from the start. Keep routes in routes.py, forms in forms.py, and configuration in config.py once the project grows past 200 lines.
Mistake 6 — Accessing Form Data Without Checking the Method
The problem:
# WRONG — crashes on GET requests
name = request.form['name']
The fix:
# CORRECT — safely handle both GET and POST
@app.route('/contact', methods=['GET', 'POST'])
def contact():
if request.method == 'POST':
name = request.form.get('name', '')
Mistake 7 — Using | safe on User-Submitted Content
The problem: {{ user_input | safe }} disables Jinja2’s automatic HTML escaping, allowing users to inject malicious JavaScript (XSS attacks).
The fix: Only use | safe on content your own application generates and fully controls. Never on anything a user submitted.
Mistake 8 — Not Reading Error Messages
The problem: Beginners often panic when they see a red Flask error page and give up or start randomly changing code.
The fix: Flask’s debug error pages are incredibly informative. Read the error type and message carefully — they almost always tell you exactly what went wrong and where.
Real-World Mini Project
Now let us put everything together and build a complete, styled, three-page personal website. This project uses every concept covered in this guide.
Project Overview
| Page | URL | Description |
|---|---|---|
| Home | / | Welcome page with your name and intro |
| About | /about | A short bio and skills section |
| Contact | /contact | A form that collects name, email, message |
Final Project Folder Structure
my_flask_site/
├── app.py
├── requirements.txt
├── static/
│ └── css/
│ └── style.css
└── templates/
├── base.html
├── index.html
├── about.html
└── contact.html
The Complete app.py
from flask import Flask, render_template, request
app = Flask(__name__)
# ─── Home Route ──────────────────────────────────────────────
@app.route('/')
def home():
return render_template(
'index.html',
page_title='Home',
name='Jane Doe',
tagline='Python Developer & Flask Enthusiast'
)
# ─── About Route ─────────────────────────────────────────────
@app.route('/about')
def about():
skills = ['Python', 'Flask', 'HTML & CSS', 'SQL', 'Git']
return render_template(
'about.html',
page_title='About',
skills=skills
)
# ─── Contact Route ───────────────────────────────────────────
@app.route('/contact', methods=['GET', 'POST'])
def contact():
success = False
submitted_name = ''
if request.method == 'POST':
name = request.form.get('name', '').strip()
email = request.form.get('email', '').strip()
message = request.form.get('message', '').strip()
if name and email and message:
success = True
submitted_name = name
return render_template(
'contact.html',
page_title='Contact',
success=success,
submitted_name=submitted_name
)
# ─── Run the App ─────────────────────────────────────────────
if __name__ == '__main__':
app.run(debug=True)
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 %}Jane Doe{% endblock %} | Flask Portfolio</title>
<link rel="stylesheet" href="{{ url_for('static', filename='css/style.css') }}">
</head>
<body>
<nav>
<span class="brand">Jane Doe</span>
<div class="nav-links">
<a href="{{ url_for('home') }}">Home</a>
<a href="{{ url_for('about') }}">About</a>
<a href="{{ url_for('contact') }}">Contact</a>
</div>
</nav>
<main>
{% block content %}{% endblock %}
</main>
<footer>
<p>Built with ❤️ using Python & Flask © 2026</p>
</footer>
</body>
</html>
templates/index.html
{% extends "base.html" %}
{% block title %}Home{% endblock %}
{% block content %}
<section class="hero">
<h1>Hi, I'm {{ name }} 👋</h1>
<p class="tagline">{{ tagline }}</p>
<p>Welcome to my personal website, built entirely with Python and Flask.</p>
<a href="{{ url_for('contact') }}" class="btn">Get In Touch</a>
</section>
{% endblock %}
templates/about.html
{% extends "base.html" %}
{% block title %}About{% endblock %}
{% block content %}
<h1>About Me</h1>
<p>I'm a Python developer who loves building web applications with Flask.
I enjoy turning ideas into real, working software.</p>
<h2>My Skills</h2>
<ul class="skills-list">
{% for skill in skills %}
<li>✅ {{ skill }}</li>
{% endfor %}
</ul>
{% endblock %}
templates/contact.html
{% extends "base.html" %}
{% block title %}Contact{% endblock %}
{% block content %}
<h1>Get In Touch</h1>
{% if success %}
<div class="success-message">
<p>✅ Thank you, {{ submitted_name }}! Your message has been received.</p>
</div>
{% endif %}
<form action="{{ url_for('contact') }}" method="POST">
<div class="form-group">
<label for="name">Your Name</label>
<input type="text" id="name" name="name" required placeholder="Jane Smith">
</div>
<div class="form-group">
<label for="email">Email Address</label>
<input type="email" id="email" name="email" required placeholder="jane@example.com">
</div>
<div class="form-group">
<label for="message">Your Message</label>
<textarea id="message" name="message" rows="6" required
placeholder="What would you like to discuss?"></textarea>
</div>
<button type="submit">Send Message 📨</button>
</form>
{% endblock %}
Running the Completed Project
# Make sure your virtual environment is active
flask run
Visit http://127.0.0.1:5000 and you will see your complete three-page personal website — styled, navigable, and with a working contact form.
🚀 What’s Next? Once your contact form is collecting submissions, you can automatically send them to your inbox using Python. This guide on how to send emails automatically using Python shows you exactly how to wire up email notifications with your Flask contact form — a genuinely useful upgrade that makes your site production-ready.
You can also persist contact form submissions by saving them to a text or CSV file. If you are not familiar with file operations in Python, this guide on how to read and write text files in Python is the perfect starting point before moving to a full database.
Frequently Asked Questions
Is Flask good for beginners?
Yes — Flask is one of the best web frameworks for Python beginners. It has a minimal learning curve, uses straightforward Python syntax, and lets you build a working website with very little boilerplate code. Unlike Django, Flask does not force you to learn ten concepts at once.
What is the difference between Flask and Django?
Flask is a micro-framework: lightweight, flexible, and minimal. You choose what to add (databases, authentication, etc.) as you need them. Django is a full-stack framework with everything built in — an ORM, admin panel, authentication system, and more. Flask is better for learning and small-to-medium projects; Django suits larger, content-heavy platforms.
Do I need to know HTML to use Flask?
Basic HTML knowledge is helpful. Flask renders HTML through Jinja2 templates, so you will be writing HTML files. However, you can start with very simple markup and improve your HTML skills as your Flask skills grow. The two learning paths complement each other well.
Can I build a full, professional website with Flask?
Absolutely. Flask scales from a simple personal page to complex, production-grade platforms. With extensions like Flask-SQLAlchemy (database), Flask-Login (user sessions), and Flask-RESTful (APIs), Flask can power sophisticated web applications.
How do I store user data in a Flask app?
For simple projects, you can save data to text or CSV files using Python’s built-in file handling. For production apps, use SQLite with Flask-SQLAlchemy (beginner-friendly) or PostgreSQL (production databases). Start simple and add complexity as your needs grow.
What Python version should I use with Flask?
Python 3.11 or later is recommended as of 2026. Flask dropped Python 2 support in Flask 2.0, and Python 3.11 delivers significant performance improvements. You can check the Flask changelog for current version compatibility details.
How do I deploy a Flask app to the internet?
Popular beginner-friendly deployment platforms include:
- Railway — simple Git-based deployment
- Render — free tier available, great for Flask
- PythonAnywhere — Python-specific hosting, beginner-friendly
- DigitalOcean App Platform — deploy directly from GitHub
What is if __name__ == '__main__' in Flask?
This Python pattern means: “only run this code if this file is executed directly (not imported by another module).” It prevents the Flask development server from starting accidentally when your app.py is imported in tests or by a production server like Gunicorn.
Conclusion
You have just covered everything a beginner needs to go from “I know Python” to “I can build real websites.” Let us recap the complete journey:
- ✅ Understood what Flask is and why it is perfect for beginners
- ✅ Set up a clean development environment with virtual environments
- ✅ Built and ran your first Flask application
- ✅ Mastered routing, dynamic URLs, and HTTP methods
- ✅ Created dynamic HTML pages with Jinja2 templates and template inheritance
- ✅ Handled real user input with HTML forms and Flask’s
requestobject - ✅ Served CSS and images using Flask’s static file system
- ✅ Organized your project with a clean, scalable folder structure
- ✅ Learned the most common beginner mistakes — and how to avoid them
- ✅ Built a complete three-page personal website with a working contact form
Flask rewards curiosity. Every concept you have learned here opens a door to something more powerful: connect a database, add user login, build a REST API, or deploy your site for the world to see.
The best next step is not just reading more — it is building. Take the mini project from this guide, make it your own, and keep adding features one at a time. That is how real developers grow.
