A block is a group of Python statements that belong together. Python uses indentation — not curly braces — to define where a block starts and ends. Understanding blocks is fundamental to reading and writing any Python code.
If you have written code in languages like JavaScript, Java, or C, you are used to curly braces { } marking where a section of code begins and ends. Python takes a different approach: it uses indentation — the whitespace at the start of a line — to communicate structure. That structure is built from blocks.
What Is a Block?
In Python, a block is a sequence of one or more statements that are grouped together and treated as a unit. The Python interpreter runs all the statements in a block together, in order, when that block is reached during execution.
Every block in Python is attached to a header line — a statement that tells Python what to do with the block. The block is always written on the lines immediately below that header line, indented inward from it.
Python uses indentation as syntax — not just style. Incorrect indentation causes an IndentationError that prevents the program from running at all.
Here is the most minimal example: a function definition with a one-line block as its body.
def greet(): # header line — ends with colon
print("Hello, world!") # block — indented 4 spaces
greet() # back at column 0 — outside the block
The print call on line 2 is the block. It is indented four spaces beneath the def header. Everything at that indentation level — until the indentation drops back — belongs to the function body.
Click a line above to see what role it plays in the block structure.
The Header Line and the Colon
Every block in Python is introduced by a header line. Header lines always end with a colon (:). The colon is Python's way of saying "a block follows." Without the colon, Python raises a SyntaxError.
Header lines appear with several Python keywords:
| Keyword | Purpose | Example header line |
|---|---|---|
def | Define a function | def calculate(x): |
if | Conditional branch | if score >= 80: |
elif | Else-if branch | elif score >= 60: |
else | Default branch | else: |
for | Iteration | for item in items: |
while | Conditional loop | while running: |
class | Define a class | class Animal: |
try | Exception handling | try: |
except | Catch an exception | except ValueError: |
with | Context manager | with open("file.txt") as f: |
When you are first learning Python, train your eye to look for the colon (:) at the end of a line. Wherever you see a colon, you know a block is about to begin on the next line.
Blocks in Functions, Conditionals, and Loops
Blocks in function definitions
When you define a function using def, everything indented beneath it is the function body — the block that runs each time the function is called.
def add(a, b): # header line
result = a + b # block starts here
return result # still in the block
total = add(3, 7) # back at module level — block has ended
print(total) # prints 10
Blocks in if statements
An if statement uses a block to hold the code that should run only when the condition is true. An optional else block holds code that runs when the condition is false.
score = 85
if score >= 80: # header line
print("Pass") # block A — runs when score >= 80
print("Well done!") # still in block A
else: # else header — at same level as if
print("Try again") # block B — runs when score < 80
print("Score recorded.") # outside both blocks
Blocks in for loops
A for loop uses a block to hold the code that runs on each iteration. The block is executed once for every item in the sequence being iterated over.
fruits = ["apple", "banana", "cherry"]
for fruit in fruits: # header line
upper = fruit.upper() # block — runs 3 times
print(upper) # also in the block
print("Done") # outside the loop block
This function should print a greeting, but it has an indentation error. Which line is wrong?
print(greeting) is not indented, so Python treats it as module-level code rather than part of the function. At that point, the variable greeting does not exist in the module scope, causing a NameError. Indent it by 4 spaces to place it inside the function block.
Nested Blocks
Blocks can be placed inside other blocks. This is called nesting. Each level of nesting requires one additional level of indentation. Python has no limit on how deeply blocks can be nested, though very deep nesting is generally a sign that code could be simplified.
def check_scores(scores): # outer block header — level 0
for score in scores: # inner block header — level 1
if score >= 80: # nested block header — level 2
print("Pass") # nested block body — level 3
else: # else header — back at level 2
print("Fail") # else block body — level 3
check_scores([90, 72, 85]) # back at level 0
Click a line above to see its nesting level and role.
Indentation Rules and Common Errors
Python enforces strict rules about indentation. Breaking those rules produces errors that prevent the program from running.
The four-space standard
PEP 8, the official Python style guide, recommends using 4 spaces per indentation level. Most editors insert 4 spaces automatically when you press Tab in a Python file. You can use a different consistent number of spaces (2 or 8 are sometimes used), but 4 is the universal convention and you should stick to it.
Never mix tabs and spaces
Python 3 treats tabs and spaces as different characters. Mixing them in the same block raises a TabError. Configure your editor to convert tabs to spaces automatically, and you will never encounter this issue.
Mixing tabs and spaces looks identical in many editors but causes a TabError at runtime. Set your editor to always convert Tab key presses into spaces when working with Python.
Every block needs at least one statement
A block cannot be empty. If you need a block that intentionally does nothing — for example, while you plan what to put there later — use the pass statement as a placeholder.
def placeholder_function():
pass # valid empty block — does nothing
if True:
pass # also valid — useful during development
Block scope
A subtle but important point for beginners: in Python, only def and class blocks create a new scope — a separate namespace for variable names. Blocks created by if, for, while, and with do not create new scopes. Variables assigned inside those blocks are still accessible in the surrounding function or module.
for i in range(3):
found = True # assigned inside the for block
print(found) # prints True — still accessible here
def my_func():
inner = "secret" # assigned inside the function block
# print(inner) # would raise NameError — inner is function-scoped
Common Block Problems and Solutions
Understanding block syntax is just the start. The real skill is recognising when your block structure is creating problems — and knowing the precise technique to fix each one. The following problems and solutions go well beyond the basics.
Problem: deeply nested blocks that are hard to follow
Nesting a for loop inside an if inside a def inside a class produces code that is technically valid but cognitively expensive. Each level of nesting shifts the reader's attention inward and makes it harder to reason about what any given line does. The standard solution is the early return (or guard clause) pattern — returning or continuing as soon as a condition fails rather than wrapping the main logic in an ever-deeper else block.
# Problem: three levels of nesting to reach the real work
def process_order(order):
if order is not None:
if order["status"] == "confirmed":
if order["items"]:
print("Processing...")
# Solution: guard clauses exit early, main logic stays at one level
def process_order(order):
if order is None:
return
if order["status"] != "confirmed":
return
if not order["items"]:
return
print("Processing...") # stays at one level — easy to read
When you notice your main logic is sitting at indentation level 3 or deeper, that is the signal to apply guard clauses. Invert the conditions, return (or continue in a loop) on failure, and let the successful path run at the top level of the function.
Problem: a long loop block doing one thing per item
A for loop whose body is a single expression — transforming each item or filtering items — can almost always be replaced with a list comprehension or generator expression. Comprehensions are not just shorter; they signal intent clearly and in many cases run faster because the looping is handled in optimised C code inside CPython.
# Loop block — three lines to build a list
scores = [45, 82, 67, 90, 55]
passing = []
for s in scores:
if s >= 60:
passing.append(s)
# Comprehension — one line, same result, clearer intent
passing = [s for s in scores if s >= 60]
# Generator — memory-efficient when you only need to iterate once
total = sum(s for s in scores if s >= 60)
The distinction matters: use a list comprehension when you need the result as a list you will index or reuse. Use a generator expression when you only need to iterate over the result once — generators produce items one at a time and never build the full list in memory.
Problem: a nested block that could be its own function
When the body of a loop or conditional grows beyond four or five lines and has a clear, single purpose, the right solution is to extract it into a named helper function. This reduces nesting, makes the outer block readable at a glance, and makes the extracted logic independently testable.
# Before: the loop body is doing too much
def send_reports(users):
for user in users:
if user["active"]:
subject = f"Report for {user['name']}"
body = f"Your score: {user['score']}"
print(f"Sending to {user['email']}: {subject}")
# After: extracted helper — outer block reads like a summary
def send_report_to_user(user):
subject = f"Report for {user['name']}"
body = f"Your score: {user['score']}"
print(f"Sending to {user['email']}: {subject}")
def send_reports(users):
for user in users:
if user["active"]:
send_report_to_user(user)
Problem: many elif branches testing the same variable
A long chain of if/elif blocks testing one variable against several literal values is repetitive and hard to extend. Python 3.10 introduced the match statement (structural pattern matching), which handles this case more expressively. For older code or simpler dispatch, a dictionary of callables is another Pythonic alternative that eliminates the block chain entirely.
# Long elif chain — repetitive and hard to extend
def describe_grade(grade):
if grade == "A":
return "Excellent"
elif grade == "B":
return "Good"
elif grade == "C":
return "Satisfactory"
else:
return "Needs improvement"
# Solution A: match statement (Python 3.10+)
def describe_grade(grade):
match grade:
case "A": return "Excellent"
case "B": return "Good"
case "C": return "Satisfactory"
case _: return "Needs improvement"
# Solution B: dictionary dispatch — no block chain at all
_GRADE_LABELS = {"A": "Excellent", "B": "Good", "C": "Satisfactory"}
def describe_grade(grade):
return _GRADE_LABELS.get(grade, "Needs improvement")
Problem: a with block that opens multiple resources
Opening several resources in sequence — a file to read and a file to write, for instance — used to require nested with blocks. Python 3.1 and later allow multiple context managers in a single with statement, eliminating one level of nesting. Python 3.10 added the parenthesised form that works cleanly across multiple lines.
# Nested with blocks — two levels of indentation
with open("input.txt") as src:
with open("output.txt", "w") as dst:
dst.write(src.read())
# Single with block — one level, cleaner (Python 3.1+)
with open("input.txt") as src, open("output.txt", "w") as dst:
dst.write(src.read())
# Parenthesised form for long lines (Python 3.10+)
with (
open("input.txt", encoding="utf-8") as src,
open("output.txt", "w", encoding="utf-8") as dst,
):
dst.write(src.read())
Problem: a try block that catches everything silently
A try/except block that catches Exception (or bare except:) and does nothing — or just passes — hides bugs that should surface. The correct approach is to catch the specific exception type you expect, handle it meaningfully, and let unexpected exceptions propagate naturally so they are visible during development and logging.
# Dangerous: catches everything and hides all errors
try:
value = int(user_input)
except:
pass
# Correct: catch only what you can handle, surface the rest
try:
value = int(user_input)
except ValueError:
print(f"Expected a number, got: {user_input!r}")
value = None
A bare except: block will catch KeyboardInterrupt, SystemExit, and MemoryError — exceptions that Python itself uses to manage the runtime. Catching those silently can prevent Ctrl+C from stopping a program and mask critical system failures.
Problem: using pass where a docstring communicates intent better
The pass statement satisfies the requirement for a non-empty block, but it communicates nothing. When writing a function or class body that is intentionally incomplete — a stub you plan to implement — a docstring is a better placeholder. It satisfies the block requirement, appears in help(), and tells the next reader what the function is supposed to do.
# pass: valid but tells readers nothing
def calculate_tax(amount, rate):
pass
# Docstring stub: satisfies the block, documents intent
def calculate_tax(amount, rate):
"""Return amount * rate. TODO: handle zero-rate and negative values."""
Reserve pass for cases where the block is genuinely a no-op by design — an exception branch you want to silently ignore for a known, intentional reason — and even then, add a comment explaining why it is intentional.
How to Write a Python Code Block
-
Write the header line
Write the statement that introduces the block — such as
if,for,while, ordef. End the line with a colon (:). The colon is mandatory; Python raises aSyntaxErrorwithout it. -
Indent the body
Press Tab (or type 4 spaces) at the start of each line that belongs inside the block. All lines in the same block must use exactly the same indentation. The first indented line after the colon is the first line of the block.
-
Keep indentation consistent
Every line in the same block must use exactly the same number of spaces. Do not mix tabs and spaces. If one line uses 4 spaces and another uses a tab character, Python raises a
TabError. -
End the block by de-indenting
To close a block, write the next line at the indentation level of the header line, or at a lower level. Python treats the drop in indentation as the end of the block — no closing brace or keyword is needed.
Build the header line for a function that takes one parameter called name:
def, the function name greet, parentheses enclosing the parameter name, and a colon to signal the start of the block.
This loop should print each number doubled, but there is a block indentation error. Which line is wrong?
doubled = n * 2 is not indented, so Python does not treat it as part of the for loop block. Python expects at least one indented statement immediately after the for header, so it raises an IndentationError. Add 4 spaces of indentation to line 3 to fix it.
Python Learning Summary Points
- 01A block is a group of Python statements that belong together, introduced by a header line ending in a colon (
:). - 02Python uses indentation — not curly braces — to define block boundaries. All lines at the same indentation level belong to the same block.
- 03The standard is 4 spaces per indentation level. Never mix tabs and spaces in the same file.
- 04A block ends when the indentation level returns to that of the header line, or drops further. No closing keyword or brace is needed.
- 05Blocks can be nested: each additional level of nesting requires one more level of indentation.
- 06Only
defandclassblocks create a new scope. Blocks fromif,for, andwhiledo not — variables inside them are accessible in the enclosing function or module. - 07Use
passas a placeholder in any block that must exist but has no statements yet. - 08An incorrect indentation raises an
IndentationErrororTabErrorbefore the program runs.
Frequently Asked Questions
A block in Python is a group of statements that belong together and are executed as a unit. Python identifies blocks through consistent indentation — all lines in the same block must be indented by the same amount relative to the header line that introduces the block.
A block starts immediately after a header line that ends with a colon (:). Header lines introduce structures like if, else, for, while, def, class, and with. The first indented line after the colon is the first line of the block.
A block ends when the indentation level returns to that of the header line, or goes even further back. Python does not use curly braces or keywords like end to close blocks — the drop in indentation is the only signal Python needs.
PEP 8, the official Python style guide, recommends 4 spaces per indentation level. Tabs are allowed but mixing tabs and spaces in the same file causes a TabError. Four spaces is the universal standard in Python code.
Python raises an IndentationError and refuses to run the code. The error message points to the line that was expected to be indented but was not. Fix it by adding 4 spaces at the start of the line.
Yes. Blocks can be nested inside other blocks. Each nested block must be indented by an additional level beyond its parent block. This is common with if statements inside for loops, or loops inside function definitions.
A header line is any statement that introduces a block. It always ends with a colon (:). Examples include if condition:, for item in items:, def function_name():, and class ClassName:.
In Python, only def and class definitions create a new scope. Blocks introduced by if, for, while, and with do not create a new scope — variables defined inside them are accessible in the enclosing function or module.