Why Is My Loop Infinite? Every Cause Explained in Python

Your program is frozen. The terminal is spewing output at impossible speed, or worse, producing nothing at all. You have an infinite loop. This article catalogs every major cause of infinite loops in Python, from the obvious to the deeply subtle. Each section shows the buggy code, explains exactly why it loops forever, and provides the fix.

Infinite loops are among the most common bugs in programming, and Python is no exception. What makes them especially frustrating is that Python will not tell you about them. Unlike a TypeError or a NameError, an infinite loop produces no traceback. The code is syntactically valid and semantically legal. It simply never stops.

The Zen of Python, written by Tim Peters in 1999 and formalized as PEP 20, includes the aphorism "Now is better than never" -- which the Python community has long interpreted as a statement that code stuck in an infinite loop is worse than code that does not run at all. Another aphorism, "Errors should never pass silently," is particularly relevant here: an infinite loop is one of the most silent errors Python can produce, because the language has no built-in mechanism to detect or prevent it.

PEP

The Zen of Python — Author: Tim Peters. Status: Active. Created August 19, 2004. Nineteen aphorisms capturing the guiding principles of Python's design. Viewable at any time by running import this in a Python interpreter.

#1 The Forgotten Counter Update

This is the most common cause of infinite loops worldwide. A while loop's condition depends on a variable, but the loop body never changes that variable. The condition is true on entry and stays true forever.

count = 0

while count < 5:
    print("Processing item", count)
    # Oops -- forgot count += 1

The variable count starts at 0, which is less than 5, so the loop body runs. But nothing inside the loop changes count. After the print, Python re-evaluates count < 5, which is still True, and the body runs again. And again. Forever.

count = 0

while count < 5:
    print("Processing item", count)
    count += 1  # This line is critical

The textbook "How to Think Like a Computer Scientist: Learning with Python 3" by Peter Wentworth, Jeffrey Elkner, Allen B. Downey, and Chris Meyers describes this as the most common and arguably the most frustrating kind of error that occurs with while loops. Their recommended debugging approach is to add a print statement at the end of the loop body that displays the values of the variables in the condition and the value of the condition itself. If the loop keeps going, you can see at a glance why the condition never becomes false.

The Wrong-Direction Variant

A more subtle version occurs when the variable is updated but in the wrong direction:

x = 10

while x > 0:
    print(x)
    x += 1  # Should be x -= 1

The condition is x > 0, but x is being incremented, not decremented. It starts at 10 and climbs to 11, 12, 13 -- moving further from zero with every iteration. The fix is to ensure your update step moves the variable toward the boundary defined in the condition, not away from it.

#2 The Condition That Can Never Be False

Sometimes the loop condition is constructed in a way that makes it logically impossible to become False, regardless of what the loop body does.

user_input = ""

while user_input != "yes" or user_input != "no":
    user_input = input("Enter yes or no: ")

This looks correct at first glance: keep asking until the user enters "yes" or "no". But read the condition carefully. It says: keep looping while the input is not "yes" OR is not "no". If the user types "yes", then user_input != "yes" is False, but user_input != "no" is True. Since False or True is True, the loop continues. If the user types "no", the same logic applies in reverse. There is no possible string that makes both conditions False simultaneously, so the or always evaluates to True.

user_input = ""

while user_input != "yes" and user_input != "no":
    user_input = input("Enter yes or no: ")

Now the loop continues only while the input is neither "yes" nor "no." The moment the user enters either one, one of the conditions becomes False, making the entire and expression False, and the loop exits. You could also write this more readably with while user_input not in ("yes", "no").

De Morgan's Law in Action

This bug is a textbook case of confusing or and and in negated conditions. De Morgan's Laws state that not (A or B) is equivalent to (not A) and (not B). When you want the loop to exit when the input is "yes" or "no", the negation of that exit condition uses and, not or.

#3 The Floating-Point Equality Trap

This cause is the most insidious on the list because the code looks perfectly correct. The bug is not in your logic -- it is in the way computers represent numbers.

x = 0.0

while x != 1.0:
    print(f"{x:.20f}")
    x += 0.1

You would expect this loop to run exactly 10 times: 0.0, 0.1, 0.2, ... , 0.9, and then stop when x reaches 1.0. But it never stops. The reason is that 0.1 cannot be represented exactly in IEEE 754 binary floating-point arithmetic. The official Python documentation on floating-point limitations states plainly that the decimal number 0.1 is a repeating fraction in binary and cannot be represented exactly. The value actually stored is the nearest representable binary fraction, which is 0.1000000000000000055511151231257827021181583404541015625.

Each time you add this slightly-too-large value to x, the error accumulates. After 10 additions, x is not 1.0 but something like 0.9999999999999999 or 1.0000000000000002. It skips right past 1.0 without ever equaling it exactly, and the != check remains True indefinitely.

The SEI CERT C Coding Standard (maintained by Carnegie Mellon University's Software Engineering Institute) includes a rule -- FLP30-C -- that warns against using floating-point variables as loop counters for precisely this reason. Although written for C, the underlying IEEE 754 arithmetic is identical in Python. Their example demonstrates that a loop incrementing by 0.1 from 0.0 to 1.0 may execute 9 or 10 times depending on the platform, and using equality as the termination condition can produce a loop that never terminates.

# Option 1: Use an integer counter and derive the float
for i in range(10):
    x = i * 0.1
    print(f"{x:.1f}")

# Option 2: Use a less-than comparison instead of equality
x = 0.0
while x < 1.0:
    print(f"{x:.1f}")
    x += 0.1

# Option 3: Use math.isclose() for precise comparison
import math
x = 0.0
while not math.isclose(x, 1.0, abs_tol=1e-9):
    print(f"{x:.1f}")
    x += 0.1

Option 1 is the gold standard: never use a float as a loop counter at all. Derive the floating-point value from an integer that counts cleanly. Option 2 replaces != with <, which tolerates the small accumulated error because it only cares about magnitude, not exact equality. Option 3 uses the math.isclose() function (added in Python 3.5 via PEP 485) for approximate comparison with a configurable tolerance.

The Rule

Never use == or != as a loop termination condition with floating-point numbers. Use <, >, <=, >=, or math.isclose() instead. This applies to all languages that use IEEE 754 arithmetic, which is virtually every language on every modern CPU.

#4 Mutating a List While Iterating Over It

This one turns a for loop -- which normally cannot be infinite -- into one that never ends. It works by growing the collection the loop is iterating over.

items = [1, 2, 3]

for item in items:
    print(item)
    items.append(item)  # Infinite loop!

Python's for loop iterates over a list by maintaining an internal index counter. It starts at index 0 and increments by 1 each iteration. It stops when the index reaches the length of the list. But every time the loop body runs, items.append(item) adds a new element to the end of the list, increasing its length by 1. The index advances by 1, but the length advances by 1 too. The index can never catch up to the length, and the loop runs forever.

The Ruff linter (the fast Python linter written in Rust by Astral) includes a specific rule for this: B909 (loop-iterator-mutation). Its documentation states directly that mutating an iterable during iteration can lead to unexpected behavior, including skipped elements or infinite loops, and provides the exact example of appending to a list inside a for loop.

Interestingly, Python's behavior here is inconsistent across collection types. Dictionaries and sets will raise a RuntimeError if you change their size during iteration:

d = {"a": 1, "b": 2}

for key in d:
    d["new_key"] = 99
# RuntimeError: dictionary changed size during iteration

But lists do not have this protection. They silently allow you to append, insert, or remove elements during iteration, leading to infinite loops or skipped elements with no warning at all.

# Option 1: Iterate over a slice copy
for item in items[:]:     # items[:] creates a shallow copy
    print(item)
    items.append(item)   # Modifies original, not the copy

# Option 2: Build a new list instead of mutating
new_items = []
for item in items:
    new_items.append(item)
    new_items.append(item)  # Double each element

# Option 3: Use a list comprehension
doubled = [x for item in items for x in (item, item)]

The items[:] slice creates a snapshot of the list at the moment the loop begins. The for loop iterates over that frozen copy while you are free to mutate the original. This is the standard Python idiom and the one recommended in the official Python tutorial documentation.

#5 while True With an Unreachable break

Using while True is a perfectly valid Python pattern for loops where the exit condition is checked inside the body rather than in the header. But it becomes an infinite loop the moment the break statement becomes unreachable.

while True:
    data = fetch_from_sensor()

    if data is None:
        break

    process(data)

This code looks safe: it breaks when fetch_from_sensor() returns None. But what if that function never returns None? Perhaps it returns an empty string, or 0, or an empty list when there is no data. Perhaps it raises an exception instead. In any of those cases, the break is never reached, and the loop runs forever.

MAX_ITERATIONS = 10_000

for _ in range(MAX_ITERATIONS):
    data = fetch_from_sensor()

    if not data:  # Catches None, "", 0, [], etc.
        break

    process(data)
else:
    print("Warning: loop hit maximum iteration limit")

This version replaces while True with for _ in range(MAX_ITERATIONS), which guarantees the loop will end after a bounded number of iterations even if the break is never hit. The for...else construct lets you detect when the limit was reached without the break firing. The falsy check if not data is also broadened to catch empty strings, zero, empty lists, and other falsy values -- not just None.

This defensive pattern is especially important in production code dealing with external I/O (network calls, hardware sensors, file reads) where the behavior of the data source is not fully under your control.

#6 Infinite Recursion: The Loop That Is Not a Loop

Recursion is not a loop, but it looks and feels like one -- and when it goes wrong, it behaves like an infinite loop. A recursive function that lacks a correct base case will call itself endlessly.

def factorial(n):
    return n * factorial(n - 1)

factorial(5)

When n reaches 0, the function does not stop. It calls factorial(-1), then factorial(-2), and so on, descending into negative numbers forever. In most languages, this would eventually crash the process with a stack overflow. Python, however, has a built-in safety mechanism.

The Python interpreter sets a default recursion limit of 1000, as documented in the sys module. The official Python documentation for sys.getrecursionlimit() states that this limit prevents infinite recursion from causing an overflow of the C stack and crashing Python. When your function exceeds this depth, Python raises a RecursionError:

RecursionError: maximum recursion depth exceeded
def factorial(n):
    if n <= 1:        # Base case: stop recursing
        return 1
    return n * factorial(n - 1)

print(factorial(5))  # 120

You can check the current recursion limit with sys.getrecursionlimit() and change it with sys.setrecursionlimit(). But increasing the limit is treating the symptom, not the cause. The documentation explicitly warns that a too-high limit can lead to a crash. If your algorithm requires deeper recursion than the default allows, consider converting it to an iterative approach using an explicit stack data structure, or use memoization to prevent redundant calls.

The Accidental Mutual Recursion

A subtler form of infinite recursion occurs when two or more functions call each other in a cycle:

def is_even(n):
    if n == 0:
        return True
    return is_odd(n - 1)

def is_odd(n):
    if n == 0:
        return False
    return is_even(n - 1)

is_even(-1)  # Infinite recursion: -1, -2, -3, ...

This works for positive integers, but passing -1 sends it into a spiral of negative numbers that never hit 0. The fix is to validate the input or add a guard: if n < 0: raise ValueError("n must be non-negative").

#7 Updating the Wrong Variable

This is a typo-class bug that is embarrassingly easy to write and surprisingly difficult to spot during code review.

total = 0
i = 0

while i < 100:
    total += i
    totol += 1  # Typo! Should be i += 1

Wait -- this will actually raise a NameError because totol is not defined. Python catches this particular typo. But consider a slightly different version where the typo lands on an existing variable:

total = 0
i = 0

while i < 100:
    total += i
    total += 1  # Oops -- incremented total instead of i

Now there is no error. The code runs, total grows rapidly, and i stays at 0 forever. Python cannot catch this because it does not know which variable you intended to update. This is a semantic error -- the code does exactly what you wrote, just not what you meant.

Prevention Strategy

Use descriptive variable names instead of single letters. A loop controlled by retry_count is much harder to accidentally confuse with total_bytes than i is with t. Linters and type checkers cannot catch this kind of bug, but good naming conventions can prevent it.

How to Debug an Infinite Loop

When your program is stuck, you need a systematic approach to find the problem. Here are the techniques that work, in order of escalating power.

1. Add Diagnostic Print Statements

Print the loop's control variables and the condition at the top of every iteration. This is the single most effective debugging technique for infinite loops because it makes the invisible visible:

while some_condition:
    print(f"DEBUG: x={x}, y={y}, condition={some_condition}")
    # ... rest of loop body

If the printed values never change, your counter update is missing or broken. If they change but the condition never becomes False, your condition logic is wrong. If the values oscillate or diverge, you may have a floating-point problem.

2. Add a Safety Counter

Temporarily add a hard limit to prevent the loop from running forever while you investigate:

_safety = 0
while some_condition:
    _safety += 1
    if _safety > 10_000:
        raise RuntimeError("Safety limit hit -- possible infinite loop")
    # ... rest of loop body

This transforms a silent infinite loop into a loud, traceable error with a full stack trace pointing to the exact location.

3. Use a Debugger

Python's built-in pdb debugger lets you step through loop iterations one at a time and inspect variables at each step. Place breakpoint() (Python 3.7+) inside the loop body, and when the program hits it, you can use the n command to advance one line, p variable_name to print a variable, and c to continue to the next breakpoint. IDE debuggers like those in VS Code or PyCharm provide the same capability with a visual interface, making it easy to watch how loop variables evolve across iterations.

4. Use a Linter

Static analysis tools can catch some infinite loop patterns before you even run the code. Ruff's rule B909 detects list mutation during iteration. Pylint's W0120 warns about else clauses on loops without break. These are not perfect -- no linter can detect all infinite loops (this is provably impossible, a consequence of the Halting Problem proved by Alan Turing in 1936) -- but they catch the most common patterns.

When Infinite Loops Are Intentional

Not all infinite loops are bugs. Many real-world programs are designed to run indefinitely: web servers, game loops, event listeners, operating system kernels. The standard Python pattern for an intentional infinite loop is while True with an explicit break, return, or sys.exit() as the exit mechanism:

while True:
    command = input(">>> ").strip()

    if command == "quit":
        print("Goodbye.")
        break

    execute(command)

The key difference between an intentional and an accidental infinite loop is intent and control. An intentional loop has a documented, reachable exit path. An accidental loop is missing one. If you write a while True loop, make sure the break condition is actually achievable for all possible inputs.

The Infinite Loop Cheat Sheet

Here is every cause at a glance, with its one-line fix:

# 1. Forgotten counter update
#    Fix: Add the missing increment/decrement

# 2. Condition can never be False (or/and confusion)
#    Fix: Apply De Morgan's Laws; use 'and' not 'or'

# 3. Floating-point equality comparison
#    Fix: Use < or > instead of ==, or math.isclose()

# 4. Appending to a list while iterating over it
#    Fix: Iterate over a copy: for item in items[:]

# 5. while True with unreachable break
#    Fix: Add a safety counter or broaden the exit check

# 6. Missing base case in recursion
#    Fix: Add the base case; validate inputs

# 7. Updating the wrong variable
#    Fix: Use descriptive names; add diagnostic prints

Every infinite loop falls into one of these categories. The counter does not change. The condition cannot flip. The numbers do not compare correctly. The collection keeps growing. The exit is unreachable. The recursion has no floor. Or the wrong variable got updated. Once you can recognize these seven patterns, you can diagnose any stuck program in minutes instead of hours.

Key Takeaway

An infinite loop is a semantic error -- your code does exactly what you wrote, just not what you meant. Python cannot detect it for you. The Halting Problem, proven by Turing in 1936, guarantees that no tool ever can in the general case. The fix is always the same: understand the termination contract of your loop and verify that the contract is fulfillable. Every loop should have an answer to the question: "What specific, reachable change in state will cause this loop to end?"

cd ..