Concepts / Designing Idempotent Functions

Designing Idempotent Functions

A retry repeats the wrapped function as a whole. It does not restart only at the assertion that failed.

  • Programming
Interactive lab

Try it: A Retry Decorator, Step by Step

What a decorator does: @retry(max_attempts) turns a function into a wrapper that calls the original. When the original's assertion fails, the wrapper catches the AssertionError, logs the attempt and runs the WHOLE function again (repeating its side effect) until it succeeds or runs out of attempts, then re-raises (with a traceback whose deepest frame is the assert) or returns a default.

How it works

  1. @retry(max_attempts=3) is evaluated first: retry(3) returns decorate; decorate(save_reading) returns wrapper; the name save_reading is bound to the wrapper.
  2. Calling save_reading(42) runs the wrapper, which loops over attempts and calls the original function inside try.
  3. A false assert raises AssertionError immediately; the original stops and the exception moves up to the wrapper's except block.
  4. The wrapper logs the failed attempt and starts a new, complete execution of the original: side effects before the assert (saved.append) happen again.
  5. After the last allowed attempt the wrapper's design decides: raise last_error (the traceback's deepest frame is the assert line; the wrapper frames only caught and re-raised it) or return None.

Default run (36 steps): Line 1: def retry(max_attempts): creates the decorator factory and binds the name retry. Nothing inside it runs yet. … The program finished: the wrapper returned 'saved' from attempt 3. The side effect ran 3 times for one reading: a retried function should be idempotent.

Simplified: One fixed program; you choose the per-attempt sensor results, max_attempts (1-5), whether the write comes before or after the check, and raise vs return None. Your settings are never executed: the lab's model reproduces CPython 3.12's line-by-line execution, printed output and traceback text for all 640 possible programs (checked against real runs). The "database" is a Python list.

Educational simulation

Loading the simulation…

Why Retries Need Careful Tracing

A retry decorator can make a failed operation run again automatically. That convenience creates an important execution question: what exactly happens between the first failure and the next attempt? The wrapped function does not resume at the failed assertion. The decorator catches the exception, records the failed attempt, and re-executes the entire wrapped function. This repeated execution is the central behavior to understand when designing functions for retry-based systems.

Key point

A retry repeats the wrapped function as a whole. It does not restart only at the assertion that failed.

Attempt 1wrapped functionDatabase writefirst executionAssertionErrorcondition is FalseAttempt 2wrapped functionDatabase writesecond executionexecutesthen evaluatesretryexecutes again
What changes, and what remains relevant, when the same wrapped operation is executed again after an assertion failure?

The Assertion Failure

An assert statement checks a condition. When that condition is False, Python raises an AssertionError immediately. The current function stops at that point, and the exception begins propagating up the call stack unless an exception handler catches it.

What do you think happens?

A decorated function reaches an assert statement whose condition is False. What happens next?

  • The function continues after the assertion

  • Python raises an AssertionError and the decorator can handle it

  • Only the failed assertion is executed again

  • The caller always receives the exception immediately

Reveal answer
Answer:

Python raises an AssertionError and the decorator can handle it.

The false condition raises the exception immediately. Because the function is wrapped, the decorator is positioned to catch the exception before it reaches the caller, depending on the decorator's design.

Assertion conditionFalseAssertionErrorraised immediatelyWrapped functionstopsDecorator handlercatches exceptionevaluates Falsestopspropagates to
What is the control-flow path from a false assertion to the decorator's exception handler?

Decorator Interception

A decorator sits between the caller and the wrapped function. The caller starts the decorated call, the decorator enters its execution process, and the decorator invokes the wrapped function. If the wrapped function raises an exception, control returns to the decorator's exception handler. The decorator can then log the failure, decide whether to retry, and either invoke the function again or allow the exception to propagate.

CallerDecoratorintercepts executionWrapped functionrunsAssertionErrorraisedException handlerlogs and decidescallsinvokesraisescaught by
How does control move into the decorator, into the wrapped function, and back to the decorator after an exception?

Retry Attempt Flow

Worked example

A failed first attempt

Trace a decorated function whose first execution raises an AssertionError and whose retry executes the wrapped function again.

  1. 1
    Start

    The caller invokes the decorated function. Control first enters the decorator.

  2. 2
    First execution

    The decorator invokes the wrapped function. The function runs until its assert condition evaluates to False.

  3. 3
    Raise

    Python raises an AssertionError immediately and stops the current function.

  4. 4
    Intercept

    The decorator catches the exception, logs the failure with the attempt number, and checks whether another attempt is allowed.

  5. 5
    Re-execute

    If the retry policy allows another attempt, the decorator invokes the entire wrapped function again. It does not execute only the failed assertion.

  6. 6
    Finish

    The next execution either succeeds, fails again and reaches another retry decision, or eventually reaches the maximum number of attempts.

Result

The retry mechanism controls the transition between attempts, while the wrapped function starts over from its beginning on each re-execution.

Run functioncurrent attemptAssertionErrorfunction stopsFailure logattempt numberRetry allowedmaximum not reachedNext attemptentire functionLast exceptionor default valueraisescaught and recordedchecks policyyesre-executesno
What happens after the decorated function raises an AssertionError, and how does the retry mechanism decide whether to run it again?
Key point

A retry loop is controlled by the decorator, but the work inside each attempt belongs to the wrapped function. Any side effect performed before the failure, such as a database write, happens again when the entire function is re-executed.

Reading the Traceback

A traceback identifies where an exception occurred by showing the file name, line number, and function associated with the failure. When decorators are involved, the traceback can contain decorator frames because those frames are part of the call stack. To locate the actual source of the error, inspect the deepest frame, which is listed last. That frame shows where the exception originated.

Caller frameearlier frameDecorator frameinterceptionFunction framefile name and line numberFailed assertionorigincalls throughinvokescontains
How do the traceback's file name and line number identify the assertion that actually failed?
  1. 1
    Find the exception type, such as AssertionError.
  2. 2
    Read the traceback frames in order.
  3. 3
    Treat the deepest frame, listed last, as the location where the exception originated.
  4. 4
    Use that frame's file name and line number to locate the failed assertion in the source.
  5. 5
    Do not mistake a decorator frame for the source of the assertion failure.

Design Implications

The retry behavior gives a practical design requirement for functions that may be executed more than once: examine every operation that occurs before a possible failure. Because the decorator re-executes the complete wrapped function, side effects are not automatically limited to one execution. A database write performed before an assertion can occur again on a later attempt. Designing an idempotent function therefore requires careful attention to what repeated execution does to those operations.

Execution detailWhat happens on failureWhat happens on retry
AssertionA false condition raises AssertionErrorThe assertion is encountered again when the function starts over
Wrapped functionThe current execution stopsThe entire function is re-executed
DecoratorCatches and logs the exceptionDecides whether another attempt is allowed
Database writeMay already have occurred before the failureCan occur again during the next full execution

The retry mechanism repeats the function, including operations performed before the failure.

  • Common mistake

    Assuming a retry resumes at the failed assertion

    What it looks like

    The first attempt fails at an assertion, so the next attempt is imagined to begin on that same line.

    Why it fails

    The decorator re-executes the entire wrapped function rather than only the line that failed.

    Do this instead

    Trace the next attempt from the function's beginning and include every operation that runs before the assertion.

  • Common mistake

    Treating the decorator frame as the origin of the error

    What it looks like

    The traceback includes decorator code, so the decorator is identified as the place where the assertion failed.

    Why it fails

    Decorator frames are part of the call stack, but the deepest frame shows where the exception actually originated.

    Do this instead

    Read the last traceback frame and use its file name and line number to find the failed assertion.

  • Common mistake

    Assuming catching an exception means the operation succeeded

    What it looks like

    The decorator logs the AssertionError, so the failure is treated as resolved.

    Why it fails

    Catching lets the decorator decide whether to retry, propagate the last exception, or return a default value.

    Do this instead

    Follow the decorator's next decision after logging the failed attempt.

  • Common mistake

    Ignoring repeated side effects

    What it looks like

    A database write happens before the assertion, but the retry analysis considers only the assertion.

    Why it fails

    The entire function runs again, so the database write can happen again as well.

    Do this instead

    List the operations performed before the failure and account for each one on every attempt.

Trace It Yourself

MEDIUM

A decorated function performs a database write, reaches an assert statement whose condition is False, and raises an AssertionError. The decorator logs the failed attempt and allows another attempt. Trace the order of events from the caller's invocation through the second execution. Identify which operation can happen twice and identify which traceback frame you would inspect first to locate the assertion.

Hints
  • Begin with the decorator, not directly with the wrapped function.
  • The first attempt stops when the assertion condition is False.
  • The next attempt re-executes the whole wrapped function.
  • The deepest traceback frame identifies the originating source location.
Worked example

Practice trace answer

Determine the control flow for a database write followed by a failed assertion inside a retry-decorated function.

  1. 1
    First call

    The caller enters the decorator, which invokes the wrapped function.

  2. 2
    First write

    The wrapped function performs the database write before reaching the assertion.

  3. 3
    Failure

    The false assertion condition raises AssertionError and stops that execution of the function.

  4. 4
    Retry decision

    The decorator catches and logs the exception, then determines that another attempt is allowed.

  5. 5
    Second write

    The decorator re-executes the entire wrapped function, so the database-write step is reached again.

  6. 6
    Traceback lookup

    The deepest traceback frame points to the file name and line number where the assertion originated.

Result

The database-write operation can occur on both executions, while the deepest traceback frame identifies the failed assertion's source location.

Key Takeaways

  1. 1
    A false assert condition raises AssertionError immediately and stops the current function.
  2. 2
    A decorator sits between the caller and the wrapped function, allowing it to catch, log, and handle exceptions.
  3. 3
    A retry re-executes the entire wrapped function rather than only the failed line.
  4. 4
    The deepest traceback frame, listed last, identifies where the exception originated.
  5. 5
    Side effects such as database writes can happen again during a retry, so repeated execution must be considered when designing functions.

At a glance

  • An AssertionError begins when an assert condition is False.
  • The decorator intercepts the exception after the wrapped function stops.
  • The retry mechanism logs the failed attempt and may execute the whole function again.
  • The deepest traceback frame shows the file name and line number where the assertion originated.
  • Repeated execution can repeat side effects, including database writes.

Related concepts

More concepts starting with D