Lesson 2 of Python Basics · Updated June 2026

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.

syntax_demo.py
def check_number(n: int) -> str:
    if n > 0:
        return "positive"
    elif n < 0:
        return "negative"
    else:
        return "zero"

print(check_number(42))
$ python syntax_demo.py
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.

20–25 minutes 7 sections Beginner friendly
📝

Statements

What a statement is, when to end one, and how Python reads them.

Core
↕️

Indentation

How whitespace defines structure — and why mixing tabs and spaces breaks your code.

Core
🧱

Code Blocks

How if, for, while, and def use indentation to define their bodies.

Core
📘

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.

💡 You can put multiple statements on one line separated by semicolons — 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.

💡 Python 3.10+ also has "soft keywords" — 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, and NAME are three different identifiers
  • ✗Cannot start with a digit — 2var is invalid, but var2 is 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
⚠️ Python style favors 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")
💡 Python accepts any consistent indentation as long as it's consistent within the same block. But PEP 8's 4-space convention is not just a preference — every code formatter, linter, and editor's default assumes it. Use 4 spaces.

⚠️ 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 expandtab and set tabstop=4 to your .vimrc
⚠️ Never copy Python code from a PDF, a web page, or a chat window without checking the indentation. It often contains a mix of tabs and spaces that Python will reject.

🧱 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:

ConstructExamplePurpose
ifif x > 0:Conditional execution
elifelif x < 0:Additional conditional branch
elseelse:Fallback branch
forfor x in items:Loop over an iterable
whilewhile x < 10:Loop while a condition holds
defdef my_func():Define a function
classclass MyClass:Define a class
trytry:Start an error-handling block
exceptexcept ValueError:Handle a specific exception
finallyfinally:Always runs, error or not
withwith open(...) as f:Context-managed resource handling
matchmatch command:Pattern matching (Python 3.10+)
casecase "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.

💡 Deeply nested blocks (4+ levels) are a warning sign — they usually mean a function is doing too much and should be split into smaller functions. The "flat is better than nested" line from the Zen of Python refers to exactly this.

🤷 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")
⚠️ An empty block without 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,
}
💡 This is called "implicit line continuation" and it's strongly preferred over the alternative. Any newline inside ( ), [ ], 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."
⚠️ Backslash continuation is fragile — a single trailing space after the backslash breaks the syntax with an obscure error. Prefer parentheses when possible.

🎯 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.

Preferred
result = (
    first_value
    + second_value
    + third_value
)

Parentheses continue the statement implicitly — clear, stable, and what linters recommend.

Avoid
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.

💬 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.

# This comment explains the code below it
tax_rate = 0.08

price_with_tax = price * (1 + tax_rate)  # inline comment

# Comments can also be used to disable code temporarily:
# debug = True
💡 Good comment: # Retry because the API rate-limits aggressively during peak hours. Bad comment: # increment counter above counter += 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.

def calculate_bmi(weight_kg: float, height_m: float) -> float:
    """Calculate Body Mass Index.

    Args:
        weight_kg: Body weight in kilograms.
        height_m: Height in meters.

    Returns:
        The BMI as a float, rounded to two decimal places.

    Raises:
        ValueError: If height_m is zero or negative.
    """
    if height_m <= 0:
        raise ValueError("Height must be positive")
    return round(weight_kg / height_m ** 2, 2)

help(calculate_bmi)

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.

  • ✓Use triple quotes (""") for docstrings — a single-line docstring can use """...""" on one line
  • ✓Write docstrings for every public function, class, and module
  • ✓The first line should be a one-sentence summary ending in a period
  • ✓Leave a blank line between the summary and any extended description
💡 The """ 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:

  1. Prefix each line with # — the standard approach, recommended by PEP 8.
  2. Wrap the block in """ — technically a string literal, not a comment, but functionally similar when used inside a block. Most editors support selecting multiple lines and hitting Ctrl+/ to comment them out.
# This is a proper multi-line comment
# using one hash per line — the recommended style.
# Every line is clearly marked.

"""
This is a string literal that acts like a comment.
It's not stored anywhere so it's effectively ignored,
but linters may still warn about it.
"""
⚠️ Some tutorials recommend using """ for multi-line comments. Prefer the # form instead — it's clearer, doesn't create unused objects, and won't confuse docstring tools.

📘 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.

TypeConventionExample
Variablessnake_caseuser_name, total_count
Functionssnake_casecalculate_total(), send_email()
ClassesPascalCaseUserAccount, DataProcessor
ConstantsUPPER_SNAKE_CASEMAX_RETRIES, DEFAULT_TIMEOUT
Modulessnake_casedata_utils.py, api_client.py
Packageslowercasemypackage
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.

💡 If a line is too long, wrap it inside parentheses as shown in the Line Continuation section. Avoid backslash continuation unless absolutely necessary.

📐 Spacing Rules

  • ✓Use one space around binary operators: x = a + b — not x=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) — not f(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:

  1. Standard library imports — os, sys, json, datetime
  2. Third-party imports — requests, numpy, pandas, flask
  3. 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
💡 Each group should be separated by a blank line. This convention makes it immediately clear which imports are from the standard library, which are dependencies, and which are your own code.

🛠️ 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.

💡 Install any of these once (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.

Broken
if x > 0
    print("positive")

Error: SyntaxError: expected ':'

Fixed
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.

Broken
def greet(name):
  print("Hello")
    print(name)

Error: IndentationError: unindent does not match any outer indentation level

Fixed
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.

Broken
def future_feature():

# Nothing here yet

Error: IndentationError: expected an indented block

Fixed
def future_feature():
    pass

❌ Error 4 — Using Reserved Keywords as Names

Keywords have special meaning to Python and cannot be used as variable names.

Broken
class = "Physics 101"

Error: SyntaxError: invalid syntax

Fixed
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.

Broken
numbers = [1, 2, 3
print(numbers)

Error: SyntaxError: '[' was never closed

Fixed
numbers = [1, 2, 3]
print(numbers)
💡 Modern editors highlight matching brackets and auto-close them. If you see a "mismatched bracket" indicator, trust it — the actual syntax error may be many lines below where the compiler complains.

❓ 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.