Python Syntax & Indentation
The Rules That Make Python Readable
Python's syntax is famously clean — but the rules are strict. Learn statements, indentation, code blocks, line continuation, comments, and the PEP 8 conventions that every professional Python developer follows.
def check_number(n: int) -> str: if n > 0: return "positive" elif n < 0: return "negative" else: return "zero" print(check_number(42))
positive
📋 What This Lesson Covers
The grammatical rules that determine whether your Python code runs — or crashes with a SyntaxError
📐 Why Python's Syntax Feels Different
Unlike most programming languages, Python uses whitespace — specifically indentation — to structure code. In C, Java, or JavaScript, you wrap blocks in curly braces { } and indentation is purely cosmetic. In Python, indentation is the structure. Two lines at the same indentation level belong to the same block; a line that's more indented is part of a nested block.
This design choice is deliberate. Guido van Rossum believed that forced indentation would eliminate an entire class of bugs that plague brace-based languages — particularly the "dangling else" problem, where curly-brace placement and visual indentation disagree. Python's rule is simple: what you see is what the interpreter sees.
This lesson walks through every syntax rule you need to know: statements, indentation levels, code blocks, line continuation, comments, PEP 8 style conventions, and the most common syntax errors that trip up beginners.
Statements
What a statement is, when to end one, and how Python reads them.
CoreIndentation
How whitespace defines structure — and why mixing tabs and spaces breaks your code.
CoreCode Blocks
How if, for, while, and def use indentation to define their bodies.
PEP 8 Style
The community-agreed conventions for writing readable, idiomatic Python.
Conventions📝 What Is a Statement?
The basic unit of Python code — a single instruction the interpreter executes
🧱 Statements and Expression Statements
A statement is a single line of code that does something: assigns a value, calls a function, imports a module, or defines a class. Most statements occupy exactly one line.
x = 42 print("Hello") import math return x
Notice that Python does not require a semicolon at the end of a statement. The end of the line is the end of the statement. This is one of the most immediately visible differences from C-style languages.
x = 1; y = 2 — but PEP 8 discourages this. One statement per line is the Pythonic way.🔤 Keywords and Identifiers
Python reserves a set of keywords — words with special meaning to the interpreter. You cannot use them as variable names or function names.
import keyword print(keyword.kwlist)
The current keyword list (Python 3.13) includes: False, None, True, and, as, assert, async, await, break, class, continue, def, del, elif, else, except, finally, for, from, global, if, import, in, is, lambda, nonlocal, not, or, pass, raise, return, try, while, with, yield.
match, case, type — which are keywords only in specific contexts. You can still use them as variable names outside those contexts.🔡 Identifiers — Naming Things
An identifier is a name you give to a variable, function, class, or module. Python has strict rules:
- ✓Must start with a letter (a–z, A–Z) or underscore (
_) - ✓Can contain letters, digits, and underscores after the first character
- ✓Cannot be a keyword
- ✓Case-sensitive —
Name,name, andNAMEare three different identifiers - ✗Cannot start with a digit —
2varis invalid, butvar2is fine - ✗Cannot contain special characters like
@,-, or!
# Valid identifiers: name = "Alice" _private = "internal" counter2 = 5 my_variable_name = True # Invalid identifiers — will raise SyntaxError: # 2var = 10 # my-var = 10 # class = 10
snake_case for variables and functions, PascalCase for classes, and UPPER_SNAKE_CASE for constants — but more on that in the PEP 8 section.↕️ Indentation — Python's Structural Backbone
The single most important syntax rule in Python — and the one most different from other languages
🏗️ How Indentation Works
In Python, indentation defines which statements belong to which block. Unlike other languages that use curly braces, Python relies entirely on leading whitespace.
def greet(name): # This line is indented — it's inside the function print(f"Hello, {name}") if name: # More indented — inside the if block print("Name was provided") # Back to one level — inside the function, outside the if print("Goodbye") # Not indented — outside the function print("Done")
Every line at the same indentation level is considered part of the same block. When indentation changes, Python knows a new block has started or an existing block has ended.
📏 The 4-Space Rule
PEP 8 — Python's official style guide — mandates 4 spaces per indentation level. Not 2, not 3, not 8, and definitely not tabs.
def example(): # This is 4 spaces — correct print("Good") def bad_example(): # This is 2 spaces — technically valid, but bad style print("Avoid")
⚠️ Tabs vs. Spaces — The Infamous Python Trap
Python 3 forbids mixing tabs and spaces in the same block. This raises a TabError — which is a subclass of IndentationError.
def trap(): print("indented with spaces") print("indented with a tab") # TabError: inconsistent use of tabs and spaces in indentation
Here's the dangerous part: tabs and spaces look identical in most editors. So you can have visually aligned code that Python considers broken. The fix is simple — configure your editor to insert 4 spaces whenever you press Tab.
- ✓VS Code: bottom-right corner → click "Spaces: 4"
- ✓PyCharm: Settings → Editor → Code Style → Python → Tabs and Indents
- ✓Vim: add
set expandtabandset tabstop=4to your.vimrc
🧱 Code Blocks — Grouping Statements
Where indentation creates structure: after colons, blocks begin
🔍 What Starts a Block?
A code block is a group of statements that logically belong together. In Python, a block is introduced by a line ending with a colon (:). The lines that follow, indented one level deeper, form the block.
if temperature > 30: # Block 1 — executed only if the condition is True print("It's hot!") print("Drink water.") else: # Block 2 — executed only if the condition is False print("It's comfortable.")
Every construct in Python that introduces a block uses a colon. Here are the most common ones:
| Construct | Example | Purpose |
|---|---|---|
| if | if x > 0: | Conditional execution |
| elif | | Additional conditional branch |
| else | else: | Fallback branch |
| for | for x in items: | Loop over an iterable |
| while | while x < 10: | Loop while a condition holds |
| def | def my_func(): | Define a function |
| class | class MyClass: | Define a class |
| try | try: | Start an error-handling block |
| except | except ValueError: | Handle a specific exception |
| finally | finally: | Always runs, error or not |
| with | with open(...) as f: | Context-managed resource handling |
| match | match command: | Pattern matching (Python 3.10+) |
| case | case "start": | Match branch |
🧪 A Real Example — Nested Blocks
Blocks can be nested. Each level of nesting adds exactly one more level of indentation. Here's a function containing an if block containing a for block:
def classify_scores(scores): # Block 1 — function body (4 spaces) results = [] for score in scores: # Block 2 — for loop body (8 spaces) if score >= 90: # Block 3 — if body (12 spaces) results.append("A") elif score >= 75: # Block 3b — elif body (12 spaces) results.append("B") else: # Block 3c — else body (12 spaces) results.append("C") # Back to Block 1 (4 spaces) — after the loop ends return results print(classify_scores([95, 82, 70, 88]))
The visual structure of the code is the logical structure. Reading the code left-to-right, the indentation tells you exactly how the blocks nest.
🤷 The pass Statement
Every block must contain at least one statement. If you want to create an empty block — as a placeholder while you're writing code — use pass, which does nothing.
def future_feature(): pass # placeholder — will fill in later class EmptyClass: pass if condition: pass # do nothing for now else: print("action taken")
pass raises IndentationError: expected an indented block. Python requires something inside the block — pass fills that requirement when you genuinely want nothing to happen.⏎ Line Continuation
How to write long statements across multiple lines — the right way and the wrong way
🔗 Implicit Continuation Inside Brackets
The cleanest way to split a long statement is to wrap part of it in parentheses, brackets, or braces. Python continues the statement automatically as long as the delimiter isn't closed.
# Long function call — split across lines total = sum([ 12.99, 4.50, 8.75, 15.00, ]) # Long conditional expression status = ( "active" if user_logged_in and account_verified else "inactive" ) # Long dictionary literal config = { "host": "localhost", "port": 8080, "debug": True, }
( ), [ ], or { } is treated as whitespace, not a statement boundary.🔙 Explicit Continuation with Backslash
Outside of brackets, you can split a statement with a backslash at the end of a line. This tells Python the statement continues on the next line.
# Long boolean expression, split with a backslash is_eligible = user_age >= 18 \ and user_has_id \ and not user_banned # Long string split across lines (rare) message = "This is a very long message \ that continues here."
🎯 The Best Practice — Wrap in Parens
When you need to split a long expression, wrapping it in parentheses is the most idiomatic approach. It's what code formatters produce automatically, and it's less error-prone than backslashes.
result = (
first_value
+ second_value
+ third_value
)
Parentheses continue the statement implicitly — clear, stable, and what linters recommend.
result = first_value \
+ second_value \
+ third_value
Backslashes work, but are fragile — one trailing space breaks everything, and code formatters tend to reflow this style poorly.
📘 PEP 8 — The Python Style Guide
The community-agreed conventions that make Python code readable across the entire ecosystem
PEP 8 (Python Enhancement Proposal 8) is the official style guide for Python code. It's not enforced by the interpreter — you can write Python without ever following it — but code that ignores PEP 8 is noticeably harder to read for anyone else, and looks out of place in professional codebases. Following it is a small habit that pays off every time you read someone else's code — and every time they read yours.
🔤 Naming Conventions
PEP 8 defines specific naming styles for each type of identifier. Following these makes your code instantly recognizable to other Python developers.
| Type | Convention | Example |
|---|---|---|
| Variables | snake_case | user_name, total_count |
| Functions | snake_case | calculate_total(), send_email() |
| Classes | PascalCase | UserAccount, DataProcessor |
| Constants | UPPER_SNAKE_CASE | MAX_RETRIES, DEFAULT_TIMEOUT |
| Modules | snake_case | data_utils.py, api_client.py |
| Packages | lowercase | mypackage |
| Private (internal) | _leading_underscore | _internal_helper |
| Dunder (special) | __double_underscore__ | __init__, __repr__ |
📏 Line Length
PEP 8 recommends 79 characters maximum per line for code, and 72 characters for docstrings and comments. This keeps code readable on narrow screens and side-by-side diffs.
Many modern teams relax this to 88 or 100 characters — the default of Black, the most popular Python formatter — but 79 is still the PEP 8 standard.
📐 Spacing Rules
- ✓Use one space around binary operators:
x = a + b— notx=a+b - ✓Use no space around the
=sign in default arguments:def f(x=0): - ✓Use no space before a comma or colon:
[1, 2, 3]— not[1 , 2 , 3] - ✓Use one space after a comma:
f(a, b)— notf(a,b) - ✓Surround operators with single spaces — but allow higher precedence to bind tighter:
a = 2*x + 3*y - ✓Two blank lines between top-level functions and classes
- ✓One blank line between methods inside a class
📥 Import Organization
Imports should be grouped and ordered:
- Standard library imports —
os,sys,json,datetime - Third-party imports —
requests,numpy,pandas,flask - Local application imports —
from mypackage import mymodule
# Standard library import json import os from datetime import datetime # Third-party import requests import pandas as pd # Local from mypackage import mymodule
🛠️ Tools That Enforce PEP 8 Automatically
You don't have to memorize PEP 8 — modern tools will format your code to conform automatically, or flag violations as you type.
Black
The most popular Python formatter. Run black . in your project and every file is reformatted to PEP 8 conventions instantly. Used by thousands of professional teams.
Ruff
A fast Python linter and formatter written in Rust. Catches PEP 8 violations, unused imports, and hundreds of bug patterns. Run ruff check . to see violations.
Flake8
The classic PEP 8 linter. Slower than Ruff but very widely used. Combines PyFlakes, pycodestyle, and other checks into one tool.
autopep8
Automatically fixes PEP 8 violations in-place. Older than Black, less opinionated, but still useful for existing codebases.
pip install black) and configure your editor to run it on save. From that point on, you never think about formatting again — the tool handles it.⚠️ Common Syntax Errors — And How to Fix Them
The five mistakes every Python beginner makes, with quick fixes
❌ Error 1 — Missing Colon After a Block Header
Every line that introduces a block must end with a colon. Forgetting it is the single most common SyntaxError.
if x > 0
print("positive")
Error: SyntaxError: expected ':'
if x > 0:
print("positive")
❌ Error 2 — Inconsistent Indentation
Every line in a block must be indented by exactly the same amount. Mixing 2 spaces and 4 spaces in the same block causes a TabError or IndentationError.
def greet(name):
print("Hello")
print(name)
Error: IndentationError: unindent does not match any outer indentation level
def greet(name):
print("Hello")
print(name)
❌ Error 3 — Empty Block
A block cannot be empty. If you're not ready to write the block yet, use pass as a placeholder.
def future_feature(): # Nothing here yet
Error: IndentationError: expected an indented block
def future_feature():
pass
❌ Error 4 — Using Reserved Keywords as Names
Keywords have special meaning to Python and cannot be used as variable names.
class = "Physics 101"
Error: SyntaxError: invalid syntax
course_name = "Physics 101"
❌ Error 5 — Unclosed Brackets
Every opening bracket must have a matching closing bracket. Mismatches produce surprisingly cryptic errors because Python tries to continue the statement onto the next line.
numbers = [1, 2, 3 print(numbers)
Error: SyntaxError: '[' was never closed
numbers = [1, 2, 3] print(numbers)
➡️ What's Next
You understand Python's syntax rules — now let's put them to work
Assignment, naming, multiple assignment, and how Python really stores values.
Start lesson →The complete fundamentals — data types, operators, input/output, and your first programs.
Start lesson →int, float, str, bool, list, tuple, dict, set, and None — the building blocks of all Python data.
Start lesson →Documenting your code so both you and others can understand it months later.
Start lesson →❓ Frequently Asked Questions
Common questions about Python syntax and indentation
Guido van Rossum designed Python around the idea that readability is more important than brevity. Indentation-based blocks were inspired by earlier languages like ABC (which Guido also worked on). The design has an important consequence: you can't write code that looks right but behaves wrong because of misplaced braces. In C or Java, this is a real bug class — where the code looks like it's in a block but actually isn't. Python eliminates this entirely.
PEP 8 — the official style guide — recommends 4 spaces per indentation level. This is not enforced by the interpreter, but every standard formatter, linter, and code review assumes 4 spaces. Some codebases use 2 spaces, and legacy code sometimes has 3, but 4 is the strong convention. Never use tabs — Python 3 forbids mixing tabs and spaces in the same block, and the error it produces is confusing.
Technically yes — use a semicolon: x = 1; y = 2. But PEP 8 explicitly discourages this. One statement per line is the Pythonic way: it's easier to read, easier to debug, and produces cleaner diffs in version control. The only common exception is a compound statement like if x < 0: x = 0 — but even that is often considered too terse for readable code.
PEP 8 itself says: "A Foolish Consistency is the Hobgoblin of Little Minds" — meaning, rules can be broken when context justifies it. Existing code that doesn't follow PEP 8 doesn't need to be rewritten just to comply. However, new code should generally follow it, because consistency across a codebase is worth more than any individual stylistic preference. If in doubt, follow PEP 8 — and let a formatter like Black handle the details.
A comment (# ...) is entirely ignored by Python. It exists only for humans reading the source code. A docstring ("""...""") is a string literal that becomes an attribute of the function, class, or module it belongs to — accessible via obj.__doc__ and shown by help(). Docstrings are for documenting what a function does; comments are for explaining why a specific line of code exists. Use docstrings for every public function and class, and comments sparingly for non-obvious implementation details.
Because tabs and spaces are visually indistinguishable in most editors, but Python treats them as fundamentally different characters. A line indented with one tab and a line indented with four spaces look aligned — but Python sees them as two different indentation levels. The result is either TabError or, worse, a silent logic bug where code is inside the wrong block. The fix: configure your editor to insert 4 spaces when you press Tab. In VS Code, that's the "Spaces: 4" indicator in the bottom-right corner. Alternatively, run a code formatter like Black, which normalizes all indentation automatically.
Python 3 allows Unicode identifiers, so technically you can name a variable nombre_español or 变量 or even 🐍_count. However, PEP 8 strongly recommends ASCII-only identifiers for portability, tooling compatibility, and readability across international teams. Stick to English letters, digits, and underscores for identifiers; use non-ASCII only in string literals when the content itself requires it.
Yes — every public function, class, and module should have a docstring. Every private function (starting with _) should too, unless it's truly trivial. The docstring can be short — one sentence describing what the function does is often enough. But leaving a public function without a docstring makes it harder for others (and your future self) to use it confidently. Tools like pydocstyle can lint for missing docstrings automatically.
🗣️ Learner Voices
No comments yet — your voice starts the conversation.
💬 Comments & Docstrings
How to document your code without cluttering it
📝 Single-Line Comments
A comment starts with
#and continues to the end of the line. Python ignores everything after#until the line break. Use comments to explain why the code exists, not what it does — the code itself should make the "what" clear.# Retry because the API rate-limits aggressively during peak hours. Bad comment:# increment counterabovecounter += 1.📄 Docstrings — Documentation that Python Understands
A docstring is a special kind of string at the start of a module, function, class, or method. Unlike comments, docstrings become part of the object at runtime — they show up in
help(), in IDE tooltips, and in auto-generated documentation.Run
help(calculate_bmi)in the REPL and Python prints the docstring in a nicely formatted way — all for free, just by putting a string there.""") for docstrings — a single-line docstring can use"""..."""on one line"""style can be used as both a comment and a docstring depending on where it appears. Inside a function body, it's a docstring. Inside arbitrary code, it's just a multi-line string that gets discarded.📚 Multi-Line Comments
Python doesn't have a formal multi-line comment syntax. The most common ways to comment out multiple lines are:
#— the standard approach, recommended by PEP 8."""— technically a string literal, not a comment, but functionally similar when used inside a block. Most editors support selecting multiple lines and hittingCtrl+/to comment them out."""for multi-line comments. Prefer the#form instead — it's clearer, doesn't create unused objects, and won't confuse docstring tools.