What Is a Block in Python? Absolute Beginners Tutorial

Final Exam & Certification

Complete this tutorial and pass the 10-question final exam to earn a downloadable certificate of completion.

skip to exam

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.

Note

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.

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

block visualizer click any line to see what block it belongs to
1 def greet(name):
2 message = "Hello, " + name
3 print(message)
4 greet("Kandi")

Click a line above to see what role it plays in the block structure.

header line
block body (level 1)
nested block body (level 2)

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
defDefine a functiondef calculate(x):
ifConditional branchif score >= 80:
elifElse-if branchelif score >= 60:
elseDefault branchelse:
forIterationfor item in items:
whileConditional loopwhile running:
classDefine a classclass Animal:
tryException handlingtry:
exceptCatch an exceptionexcept ValueError:
withContext managerwith open("file.txt") as f:
Pro Tip

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.

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

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

python
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
spot the bug click the line with the error

This function should print a greeting, but it has an indentation error. Which line is wrong?

1 def say_hello(name):
2 greeting = "Hello, " + name
3 print(greeting)
4 say_hello("Kandi")
Line 3print(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.

python
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
nested block visualizer click a line to see its nesting level
1 def check_scores(scores):
2 for score in scores:
3 if score >= 80:
4 print("Pass")
5 else:
6 print("Fail")
7 check_scores([90, 72, 85])

Click a line above to see its nesting level and role.

header line (level 0)
block body (level 1)
nested (level 2–3)

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.

Warning

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.

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

python
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
check your understanding question 1 of 4

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.

python
# 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
Rule of Thumb

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.

python
# 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.

python
# 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.

python
# 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.

python
# 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.

python
# 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
Warning

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.

python
# 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

  1. Write the header line

    Write the statement that introduces the block — such as if, for, while, or def. End the line with a colon (:). The colon is mandatory; Python raises a SyntaxError without it.

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

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

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

code builder click tokens in the correct order

Build the header line for a function that takes one parameter called name:

your code will appear here...
name : ( def ) greet
The correct order is def greet(name): — the keyword def, the function name greet, parentheses enclosing the parameter name, and a colon to signal the start of the block.
spot the bug click the line with the error

This loop should print each number doubled, but there is a block indentation error. Which line is wrong?

1 numbers = [1, 2, 3, 4, 5]
2 for n in numbers:
3 doubled = n * 2
4 print(doubled)
Line 3doubled = 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 def and class blocks create a new scope. Blocks from if, for, and while do not — variables inside them are accessible in the enclosing function or module.
  • 07Use pass as a placeholder in any block that must exist but has no statements yet.
  • 08An incorrect indentation raises an IndentationError or TabError before 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.

Certificate of Completion
Final Exam
Pass mark: 80% · Score 80% or higher to receive your certificate

Enter your name as you want it to appear on your certificate, then start the exam. Your name is used only to generate your certificate and is never transmitted or stored anywhere.

Question 1 of 10