Syntax & Style
In Python, indentation is not decoration — it is the syntax. Learn the rules, the conventions every team enforces, and the tools that end formatting arguments for good.
What you'll learn
- Why Python uses indentation instead of braces, and what counts as valid
- The handful of PEP 8 rules that show up in every code review
- How Ruff and Black free you from formatting arguments forever
- When to write a comment, and when the code already speaks
Before you start
In most languages, indentation is decoration. You can indent a block by two spaces or by twelve, and the compiler shrugs — the meaning lives in the curly braces, and the whitespace is just there to please the eye. Python made a different bet, and it is the first thing that surprises newcomers: here, indentation is the syntax. There are no braces at all. The shape of your code on the page is the shape the program runs.
That sounds fragile until you sit with it. It means a Python program and its own outline are the same object — you cannot indent code one way and have it run another. Let us see exactly what that buys you, and exactly what trips people up.
Indentation marks the block
A block of code is simply the lines indented underneath a header — a line that ends in a colon, like an if, a for, or a def. Lines at the same indentation belong to the same block; step in further, and you have opened a nested block inside it. Read this and watch the two levels of indentation:
records = ["valid", "", "valid", None, "valid"]
cleaned = []
for r in records:
if r: # 4 spaces in — inside the loop
cleaned.append(r.strip()) # 8 spaces in — inside the if
else:
print("skipping empty record")
print(cleaned)
It prints:
skipping empty record
skipping empty record
['valid', 'valid', 'valid']
Trace it once and the rule clicks. Each record runs the loop body; if r: is true for the non-empty strings, so they are stripped and collected, while "" and None are falsy and fall to the else, printing the skip line. A developer arriving from C++ or Java will instinctively look for the braces and miss them — but the indentation is doing precisely the job braces do elsewhere. Once your eye adjusts, the absence of braces makes Python unusually quick to scan.
What actually breaks
The classic disaster is mixing tabs and spaces. Two lines can look aligned and yet be indented differently — a tab here, four spaces there — and Python 3 refuses to guess. It stops and tells you so:
def greet(name):
print("hi")
print("there") # only 3 spaces — does not line up with anything
IndentationError: unindent does not match any outer indentation level
The fix is gloriously simple: four spaces per level, and never a tab. Every modern editor will insert spaces when you press Tab if you ask it to once.
PEP 8 — the style guide everyone follows
PEP 8 is Python’s official style guide. You do not have to memorise it; you only need the parts that surface in every code review:
- Names —
snake_casefor variables and functions,PascalCasefor classes,UPPER_SNAKEfor constants. - Line length — 79 by the letter of PEP 8, or 88 by Black’s default. Most modern teams pick 88 or 100.
- Imports — standard library first, then third-party, then your own local modules, each group separated by a blank line.
- Blank lines — two between top-level functions and classes, one between methods.
- No spaces around
=in keyword arguments — writefunc(name="x"), notfunc(name = "x").
Here is a small module that obeys all of them at once. Read it for the shape — the import groups, the naming, the spacing — more than the logic:
import json
import os
from datetime import datetime
import requests # third-party — separated by a blank line
from myapp.utils import normalize # local — its own group
MAX_RETRIES = 3
DEFAULT_TIMEOUT = 30
def fetch_user(user_id: int) -> dict:
"""Fetch a user by id from the upstream API."""
url = f"https://api.example.com/users/{user_id}"
response = requests.get(url, timeout=DEFAULT_TIMEOUT)
return response.json()
class UserCache:
def __init__(self, ttl: int = 60) -> None:
self.ttl = ttl
self._data: dict = {}
Stop arguing about formatting — let a tool do it
Now the genuinely good news: nobody hand-formats Python any more. You install a formatter, save the file, and it conforms — every time, identically.
- Ruff — an all-in-one linter and formatter written in Rust, fast enough to run on every keystroke.
- Black — the original “opinionated” formatter, still common in older codebases.
uv tool install ruff
ruff format my_module.py # reformats in place
ruff check my_module.py # lints — unused imports, undefined names, and more
Comments — say why, not what
A good comment carries what the code cannot. If the code already shows what is happening, restating it adds noise; save your comments for the context only a human knows — a business rule, a sharp edge, a link to the ticket that explains a strange choice.
# Bad — just restates the code
i += 1 # increment i
# Good — explains a non-obvious choice
# Upstream API returns dates as 'DD/MM/YYYY' for EU tenants only.
date = parse_eu_date(payload["created_at"])
Docstrings — the triple-quoted string directly under a def — are a different thing again. They state the function’s contract: what it returns and what each parameter means. We will lean on them throughout these lessons.
The Zen of Python
Every Python install carries a short philosophy, printed by one line:
import this
The Zen of Python, by Tim Peters
Beautiful is better than ugly.
Explicit is better than implicit.
Simple is better than complex.
Complex is better than complicated.
...
Nineteen aphorisms in all, and three of them settle most real arguments: “There should be one — and preferably only one — obvious way to do it,” “Readability counts,” and “Explicit is better than implicit.” When two solutions are equally correct, choose the one that will be easier to read six months from now. That is the whole spirit of Python style.
In one breath
- Indentation is syntax in Python — the block is whatever is indented under a header line ending in
:. - Use four spaces per level, never tabs; mixing them is an
IndentationError. - PEP 8 fixes naming, line length, import order, and spacing — learn the parts that show up in review.
- Let Ruff (or Black) format for you, so the team never debates style.
- Comment the why; let docstrings carry the what.
Practice
Quick check
What’s next
With the visual rules in place, let us turn to the values you will actually manipulate — beginning with numbers, where Python hides a famous gotcha that has quietly wrecked more than one financial report.