Indentation in Python

core-python
beginner
Use indentation to group code inside if statements, loops, functions, classes, and methods.
  • Level: Beginner
  • Estimated time: 45–70 minutes
  • You will learn: Use indentation to group code blocks and debug indentation errors.
  • Practice in: Google Colab, Jupyter, or VS Code

Questions

  • Why does Python care about spaces at the beginning of a line?
  • How do indentation rules apply to if, for, while, functions, classes, and methods?
  • How can we read and fix IndentationError, TabError, and mistakes where code runs outside the intended block?

Objectives

  • Identify the header line and body of a Python block.
  • Indent code consistently with four spaces.
  • Predict which lines run inside and outside nested blocks.
  • Fix common indentation mistakes in conditionals, loops, functions, classes, and methods.

Hands-on episode: Indentation in Python

Python uses indentation as part of the language. In many languages, curly braces or keywords mark where a block starts and ends. In Python, the spaces at the start of the line do that job. This makes code readable, but it also means that spacing is not decoration. Spacing changes what the program means.

A block is a group of lines that belongs together. The body of an if statement is a block. The body of a loop is a block. The body of a function is a block. A method inside a class has a block, and that method may contain more blocks inside it.

Use this lesson as a debugging lab. Some examples work. Some examples are intentionally broken. Run each cell in Colab, read the error or output, and then fix only the indentation before changing anything else.

Example 1.1

Predict the output. Which lines belong to the if statement, and which line runs after the if statement is finished?

score = 85

if score >= 70:
    print("Passed")
    print("Keep practicing")

print("Done checking score")

Expected output:

Passed
Keep practicing
Done checking score

Explain Example 1.1

  • if score >= 70: is a header line. The colon says, “a block is coming next.”
  • The next two print lines are indented four spaces, so they are inside the if body.
  • The blank line does not end the program. It only separates ideas for the reader.
  • print("Done checking score") is back at the left margin, so it is outside the if block.
  • Because the score is 85, the condition is true, so the indented block runs.
  • The final print runs no matter what, because it is not inside the if block.

A good beginner habit is to point to the left edge of each line and ask: “Which block does this line belong to?” If two lines begin at the same column, they are usually siblings in the same block. If one line is farther to the right, it is inside a more specific block.

Challenge 1.1

NoteChallenge

Change score to 50 and run the cell again. Predict exactly which lines will print before you run.

Show one possible solution path

With score = 50, the condition score >= 70 is false. The two indented lines inside the if block do not run. The final line is outside the block, so it still runs.

score = 50

if score >= 70:
    print("Passed")
    print("Keep practicing")

print("Done checking score")

Expected output:

Done checking score

Debugging checkpoint 1.1

WarningDebugging checkpoint

If Python says IndentationError, first check the line after a colon. A colon usually means the next real line must be indented.

The most important rule is:

Header line with : first. Indented body next. Return to the previous indentation level when the block is finished.

Apply it

In the Colab notebook, create a cell named “indentation scratchpad” in Markdown. For every example in this lesson, write one sentence before running:

  • “I think this line is inside the block because…”
  • “I think this line is outside the block because…”
  • “I expect this error because…”

Key points

  • Indentation is syntax in Python, not visual decoration.
  • A colon usually introduces an indented block.
  • Lines at the same indentation level belong to the same surrounding block.
  • Four spaces per indentation level is the standard Python style.
  • Most indentation bugs are fixed by asking which block each line should belong to.

1. The basic block rule

A Python block normally starts with a header line ending in a colon. The body of the block is indented under that header. When the indentation returns to a previous level, the block is over.

Here is the shape:

header:
    body line 1
    body line 2
outside again

You will see this shape many times:

if condition:
    do_this()

for item in items:
    do_that(item)

while still_working:
    keep_going()

def function_name():
    function_body()

class ClassName:
    class_body()

The words change, but the block shape stays the same. This is why indentation is a core concept, not a small formatting detail.

This diagram shows how indentation creates nested blocks.

flowchart TD
  A[Left margin\nTop-level code] --> B[Header line ending with colon]
  B --> C[Indented body\ninside the block]
  C --> D[Same indentation\nmore lines in same block]
  C --> E[More indentation\nnested block]
  C --> F[Back to previous indentation\nblock is finished]

Example 1

ready = True

if ready:
    print("Start")
    print("Work")

print("Finish")

Read it as a map:

  • ready = True is top-level code.
  • if ready: starts a conditional block.
  • The two indented lines are inside that block.
  • The final line is back at the top level.

Common issue: missing indented body

Broken code:

ready = True

if ready:
print("Start")

The line after if ready: should be indented. Python cannot tell what belongs to the if statement, so it raises an IndentationError.

Fixed code:

ready = True

if ready:
    print("Start")

Common issue: a block that is accidentally empty

Broken code:

ready = True

if ready:

print("Start")

A blank line is allowed inside code, but it does not count as the body of the if statement. Python still expects an indented statement after the colon.

If you need a placeholder while planning code, use pass:

ready = True

if ready:
    pass

print("Start later")

pass means “do nothing here on purpose.” It is useful while sketching a program, but replace it with real code when the behavior is ready.

2. Indentation with if, elif, and else

The if, elif, and else headers must line up with each other. Their bodies are indented one level deeper.

Example 2

temperature = 18

if temperature < 10:
    print("Wear a heavy coat")
elif temperature < 20:
    print("Wear a light jacket")
else:
    print("Short sleeves are fine")

print("Weather check complete")

The three headers are siblings:

if temperature < 10:
elif temperature < 20:
else:

Each body is indented under its own header:

    print(...)

Common issue: else indented under if body

Broken code:

temperature = 18

if temperature < 10:
    print("Wear a heavy coat")
    else:
        print("Not that cold")

The else is too far to the right. Python reads it as if you are trying to put an else statement inside the if body, but else must match the if.

Fixed code:

temperature = 18

if temperature < 10:
    print("Wear a heavy coat")
else:
    print("Not that cold")

Common issue: code that should be conditional but is outside

This code runs, but the indentation changes the meaning:

logged_in = False

if logged_in:
    print("Welcome back")
print("Show private dashboard")

The dashboard line is not indented, so it runs even when logged_in is false. That is not a syntax error. It is a logic error.

Fixed code:

logged_in = False

if logged_in:
    print("Welcome back")
    print("Show private dashboard")
WarningCommon mistake

Not every indentation bug creates an error message. Sometimes the code runs, but a line belongs to the wrong block. When output surprises you, inspect the left edge of the lines before changing conditions or values.

Checkpoint quiz

3. Indentation with for loops

A for loop repeats its indented body once for each item. Lines inside the loop are indented. Lines after the loop return to the previous indentation level.

Example 3

names = ["Ada", "Grace", "Katherine"]

for name in names:
    print("Hello", name)
    print("This line is inside the loop")

print("All greetings sent")

Expected output:

Hello Ada
This line is inside the loop
Hello Grace
This line is inside the loop
Hello Katherine
This line is inside the loop
All greetings sent

The first two print lines repeat because they are inside the loop. The final line prints once because it is outside the loop.

Common issue: the accumulator is inside the loop

This code is broken logically:

scores = [10, 20, 30]

for score in scores:
    total = 0
    total = total + score

print(total)

It prints 30, not 60, because total = 0 runs at the start of every loop trip. The accumulator resets again and again.

Fixed code:

scores = [10, 20, 30]
total = 0

for score in scores:
    total = total + score

print(total)

The line total = 0 belongs before the loop because it sets up the starting state once. The update belongs inside the loop because it should happen once per score.

Common issue: the loop body is too short

This code runs, but only the last value is summarized:

prices = [4, 6, 8]

for price in prices:
    tax = price * 0.10
print("tax:", tax)

The final print is outside the loop. It prints once, after the loop, using the last value of tax.

If you want one line per price, indent the print:

prices = [4, 6, 8]

for price in prices:
    tax = price * 0.10
    print("tax:", tax)

If you want only the last tax after the loop, leave it unindented, but name the choice clearly:

prices = [4, 6, 8]

for price in prices:
    last_tax = price * 0.10

print("last tax:", last_tax)

Challenge 3.1

NoteChallenge

Move the final print(total) line inside the loop. Predict how many lines will print and what each line will show.

Show the explanation
scores = [10, 20, 30]
total = 0

for score in scores:
    total = total + score
    print(total)

Expected output:

10
30
60

Inside the loop, print(total) runs after every update, so it shows the running total. Outside the loop, it would show only the final total.

4. Indentation with while loops

A while loop repeats as long as its condition is true. Indentation decides what repeats. This is especially important because a misplaced update can create an infinite loop.

Example 4

countdown = 3

while countdown > 0:
    print(countdown)
    countdown = countdown - 1

print("Go")

Expected output:

3
2
1
Go

The update line countdown = countdown - 1 is inside the loop, so the condition will eventually become false.

Common issue: update outside the loop

Broken code:

countdown = 3

while countdown > 0:
    print(countdown)

countdown = countdown - 1

The update is outside the loop, so countdown stays 3 forever while the loop is running. In Colab, stop the cell if it keeps printing. Then move the update line into the loop body.

Fixed code:

countdown = 3

while countdown > 0:
    print(countdown)
    countdown = countdown - 1

Common issue: break and continue at the wrong level

break and continue only make sense inside a loop.

Broken code:

items = ["a", "stop", "c"]

for item in items:
    print(item)

    if item == "stop":
        print("found stop")

break

The break is at the top level, outside the for loop. Python raises a syntax error because there is no loop to break out of at that indentation level.

Fixed code:

items = ["a", "stop", "c"]

for item in items:
    print(item)

    if item == "stop":
        print("found stop")
        break

The break is inside the if, and the if is inside the loop. That means break runs only when the loop reaches the stop item.

5. Indentation with functions

A function definition has a header line and an indented body. Code inside the body runs only when the function is called. Code outside the body runs normally as the program reaches it.

Example 5

def greet(name):
    message = f"Hello, {name}!"
    return message

print(greet("Ada"))

The function body is:

    message = f"Hello, {name}!"
    return message

The call is outside the function:

print(greet("Ada"))

Common issue: function body is not indented

Broken code:

def greet(name):
message = f"Hello, {name}!"
return message

Python raises IndentationError because a function definition needs an indented body.

Fixed code:

def greet(name):
    message = f"Hello, {name}!"
    return message

Common issue: return outside the function

Broken code:

def add_one(number):
    result = number + 1

return result

The return line is not indented, so it is outside the function. Python raises SyntaxError: 'return' outside function.

Fixed code:

def add_one(number):
    result = number + 1
    return result

Common issue: calling the function inside its own body by accident

This code is valid, but it hides the function call inside the function body:

def greet(name):
    message = f"Hello, {name}!"
    print(message)
    greet("Ada")

The call greet("Ada") is indented inside greet, so calling greet would call itself again and again. That is recursion, and it is not what beginners usually intend here.

Move the call outside the function:

def greet(name):
    message = f"Hello, {name}!"
    print(message)

greet("Ada")

Challenge 5.1

NoteChallenge

Write a function named is_passing(score) that returns True when the score is at least 70 and False otherwise. Use indentation to put the return lines in the correct blocks.

Show one solution
def is_passing(score):
    if score >= 70:
        return True
    else:
        return False

print(is_passing(85))
print(is_passing(50))

The if and else headers are inside the function. The return lines are inside the matching conditional bodies.

6. Indentation with classes and methods

A class body is indented under the class header. Methods are functions defined inside a class, so method definitions are indented under the class, and method bodies are indented under the method definition.

Example 6

class Counter:
    def __init__(self):
        self.value = 0

    def increment(self):
        self.value = self.value + 1

    def report(self):
        return f"Counter value: {self.value}"

counter = Counter()
counter.increment()
print(counter.report())

There are three indentation levels:

class Counter:                 # level 0
    def __init__(self):        # level 1, inside class
        self.value = 0         # level 2, inside method

The lines after the class return to the left margin:

counter = Counter()
counter.increment()
print(counter.report())

Those lines are outside the class. They create and use an object.

Common issue: method is not inside the class

This code runs, but increment is not a method of Counter:

class Counter:
    def __init__(self):
        self.value = 0

def increment(self):
    self.value = self.value + 1

The increment definition starts at the left margin, so it is a regular function, not a method inside Counter.

That difference becomes visible when you try to call it as a method:

class Counter:
    def __init__(self):
        self.value = 0

def increment(self):
    self.value = self.value + 1

counter = Counter()
counter.increment()

Python creates a Counter object, but that object does not know about increment. The function named increment exists at the top level, beside the class, not inside the class. The likely error is:

AttributeError: 'Counter' object has no attribute 'increment'

This error can be confusing because you can see a function named increment in the cell. The indentation is the clue: Python does not ask “is there a function nearby with this name?” It asks “does this object have a method with this name?” For counter.increment() to work, the def increment(...) line must be indented inside the class Counter: block.

Fixed code:

class Counter:
    def __init__(self):
        self.value = 0

    def increment(self):
        self.value = self.value + 1

counter = Counter()
counter.increment()
print(counter.value)

Expected output:

1
NoteDebugging routine

When a method call raises AttributeError, check two things before changing the method body:

  1. Is the def method_name(...) line indented inside the class?
  2. Did you spell the method name the same way in the class and in the call?
  3. Is the method accidentally indented inside another method?

Common issue: method is inside another method by accident

There is a second version of the same mistake. Sometimes the method is not all the way outside the class. Instead, it is accidentally placed inside another method.

Broken code:

class Counter:
    def __init__(self):
        self.value = 0

        def increment(self):
            self.value = self.value + 1

counter = Counter()
counter.increment()

At first glance, this can look close to correct because increment is visually near the class. But count the indentation levels:

class Counter:                 # level 0
    def __init__(self):        # level 1, method inside class
        self.value = 0         # level 2, inside __init__
        def increment(self):   # level 2, also inside __init__

The line def increment(self): lines up with self.value = 0, not with def __init__(self):. That means Python treats increment as a local function created while __init__ is running. It is not a method stored on the Counter class, so this line:

counter.increment()

will likely raise:

AttributeError: 'Counter' object has no attribute 'increment'

The fix is to move def increment(self): one indentation level to the left, so it lines up with the other method definition:

class Counter:
    def __init__(self):
        self.value = 0

    def increment(self):
        self.value = self.value + 1

counter = Counter()
counter.increment()
print(counter.value)

Expected output:

1
TipHow to spot this quickly

Inside a class, method definitions usually line up with each other. If def increment(...) is farther to the right than def __init__(...), then increment is probably inside __init__ by accident.

Common issue: method body is not indented enough

Broken code:

class Counter:
    def __init__(self):
    self.value = 0

The assignment should be inside the method body. It needs one more indentation level than the method header.

Fixed code:

class Counter:
    def __init__(self):
        self.value = 0

Common issue: object code accidentally inside the class

This code is valid in some cases but usually not what beginners intend:

class Counter:
    def __init__(self):
        self.value = 0

    counter = Counter()

The line counter = Counter() is indented inside the class body. It tries to create a Counter while Python is still building the Counter class. Move object-creation code outside the class:

class Counter:
    def __init__(self):
        self.value = 0

counter = Counter()
WarningCommon mistake

Classes often have two nested levels before any real behavior happens: method definitions inside the class, then method bodies inside each method. Count the levels before editing.

7. Tabs, spaces, and inconsistent indentation

Python can technically read tabs, but the standard style is four spaces. Mixing tabs and spaces can produce errors that are hard to see because a tab may look like several spaces in your editor.

Common issue: inconsistent indentation

Broken code, with inconsistent whitespace:

if True:
    print("four spaces")
    print("tab")

Python may raise TabError: inconsistent use of tabs and spaces in indentation. The fix is not to count invisible characters by hand. Use your editor to convert tabs to spaces.

In VS Code:

  1. Open the command palette.
  2. Search for Convert Indentation to Spaces.
  3. Use four spaces per indent level.
  4. Save and run again.

In Colab, avoid pressing Tab at the beginning of a line unless you know how the notebook handles it. If indentation looks strange, delete the whitespace at the start of the line and press Space four times.

Common issue: indentation width changes inside the same block

Broken code:

if True:
    print("line one")
  print("line two")

The second print line is indented two spaces, not four. Python sees it as a mismatched indentation level.

Fixed code:

if True:
    print("line one")
    print("line two")

The exact number of spaces matters less than consistency, but four spaces is the community standard and the style used throughout this course.

8. Nested blocks

A nested block is a block inside another block. You have already seen this with loops inside functions, if statements inside loops, and methods inside classes. Each nested level moves four more spaces to the right.

Example 7

def summarize_scores(scores):
    total = 0

    for score in scores:
        if score >= 70:
            print("passing score:", score)
        else:
            print("needs review:", score)

        total = total + score

    return total

print(summarize_scores([80, 55, 90]))

Indentation levels:

def summarize_scores(scores):        # level 0
    total = 0                         # level 1, function body
    for score in scores:              # level 1, function body
        if score >= 70:               # level 2, loop body
            print(...)                # level 3, if body
        else:                         # level 2, matches if
            print(...)                # level 3, else body
        total = total + score         # level 2, loop body
    return total                      # level 1, function body
print(...)                            # level 0, outside function

This example is worth tracing slowly. Ask three questions for each line:

  1. Is this line inside the function?
  2. Is this line inside the loop?
  3. Is this line inside the if or else body?

Challenge 8.1

NoteChallenge

Move return total four spaces to the right so it is inside the loop. Predict what changes before running.

Show the explanation

If return total is inside the loop, the function returns during the first loop trip. That means it never processes the remaining scores. The code may run, but the logic is wrong.

Broken version:

def summarize_scores(scores):
    total = 0

    for score in scores:
        total = total + score
        return total

print(summarize_scores([80, 55, 90]))

Expected output:

80

Fixed version:

def summarize_scores(scores):
    total = 0

    for score in scores:
        total = total + score

    return total

print(summarize_scores([80, 55, 90]))

Expected output:

225

9. A debugging checklist for indentation problems

Use this checklist when code looks correct but Python complains about spacing or runs lines at the wrong time.

Symptom Likely cause First thing to check
IndentationError: expected an indented block A header line ending in : has no indented body Look at the first real line after the colon
IndentationError: unexpected indent A line is indented even though no block is open Look at the previous non-blank line
IndentationError: unindent does not match any outer indentation level A line returned to a column Python does not recognize Check spaces/tabs and block levels
TabError Tabs and spaces are mixed Convert indentation to spaces
Code runs too often A line is inside a loop by accident Check whether it should be left of the loop body
Code runs too rarely A line is outside a loop or conditional by accident Check whether it should be indented under the header
return outside function return is not inside a function body Indent it under the correct def
break outside loop break is not inside a loop body Indent it under the correct for or while
Method missing from class def method(...) starts at the left margin Indent the method under class
Method missing from object def method(...) is nested inside another method Line up method definitions with each other

Example 8: diagnose before fixing

Broken code:

class StudyLog:
    def __init__(self):
        self.minutes = []

    def add(self, minutes):
        if minutes > 0:
            self.minutes.append(minutes)
        else:
        print("minutes must be positive")

    def total(self):
        total = 0
        for minutes in self.minutes:
        total = total + minutes
        return total

log = StudyLog()
log.add(20)
log.add(-5)
print(log.total())

Before fixing, identify the problem lines:

  1. print("minutes must be positive") should be inside the else body.
  2. total = total + minutes should be inside the for loop body.
  3. return total should be inside the method but outside the loop.

Fixed code:

class StudyLog:
    def __init__(self):
        self.minutes = []

    def add(self, minutes):
        if minutes > 0:
            self.minutes.append(minutes)
        else:
            print("minutes must be positive")

    def total(self):
        total = 0
        for minutes in self.minutes:
            total = total + minutes
        return total

log = StudyLog()
log.add(20)
log.add(-5)
print(log.total())

Expected output:

minutes must be positive
20

This example combines class, method, conditional, loop, and return indentation. It is a good final check because it uses several block levels at once.

10. Style rules that prevent indentation bugs

PEP 8, Python’s style guide, recommends four spaces per indentation level. This course follows that rule.

Good habits:

  • Use four spaces for each block level.
  • Configure your editor to insert spaces when you press Tab.
  • Keep related lines at the same indentation level.
  • Do not align code by hand with random numbers of spaces.
  • Let formatters such as Ruff format handle mechanical spacing when possible.
  • Fix indentation before debugging values, conditions, or algorithms.

Avoid this style:

if True:
 print("one space")
 print("still works sometimes, but do not do this")

Prefer this style:

if True:
    print("four spaces")
    print("clear block")

You will occasionally see compact one-line statements like this:

if ready: print("Start")

Python allows it, but beginners should avoid it. Separate lines make the block structure visible:

if ready:
    print("Start")

Final challenge

NoteChallenge

Create a small Notebook class with three methods:

  1. __init__ creates an empty list named notes.
  2. add(note) appends the note only if it is not an empty string.
  3. show() loops over notes and prints each one.

Use an if, a for loop, a class, and methods. Then intentionally break one indentation level, run the cell, read the error, and fix it.

Show one possible solution
class Notebook:
    def __init__(self):
        self.notes = []

    def add(self, note):
        if note != "":
            self.notes.append(note)
        else:
            print("empty note ignored")

    def show(self):
        for note in self.notes:
            print("-", note)

notebook = Notebook()
notebook.add("Practice indentation")
notebook.add("")
notebook.add("Run small examples")
notebook.show()

Expected output:

empty note ignored
- Practice indentation
- Run small examples

The class body is indented under class Notebook. Each method is indented inside the class. Each method body is indented inside its def line. The if and for bodies are one level deeper inside their methods.

Check your understanding

Key points

TipKey points
  • Python uses indentation to decide which lines belong to a block.
  • A header line ending in : usually requires an indented body.
  • if, elif, and else headers that belong together should line up.
  • Loop setup often belongs before the loop; loop updates often belong inside the loop.
  • Function bodies are indented under def; return must be inside the function.
  • Methods are indented under class; method bodies are indented under method definitions.
  • Method definitions inside the same class usually line up with each other.
  • Use four spaces per indentation level and avoid mixing tabs and spaces.
  • When debugging indentation, inspect block structure before changing program logic.

References

  • Python Tutorial: control flow: https://docs.python.org/3/tutorial/controlflow.html
  • Python Tutorial: defining functions: https://docs.python.org/3/tutorial/controlflow.html#defining-functions
  • Python Tutorial: classes: https://docs.python.org/3/tutorial/classes.html
  • PEP 8 indentation guidance: https://peps.python.org/pep-0008/#indentation
Back to top