LESSON 131 · DEEPER PYTHON30 min

Build Stateful Callable Objects with __call__

Use __call__ to make a validated counter object invokable while it keeps its own state.

Example check: Worked and guided code samples are checked against CPython 3.13. Practice uses Pyodide 0.29.5 (Python 3.13) in your browser. How we check lessons

Video lesson: Build Stateful Callable Objects with __call__

The preview is stored on this site. YouTube loads only when you press Play. Watch on YouTube

Read the video transcript

Select a timestamp to watch that moment on YouTube.

  1. 00:00

    A function can hold state in a closure, but a small class makes that state inspectable. Defining __call__ lets an instance respond to parentheses like a function. A counter can remember its current number between invocations while still exposing named methods and attributes. Greeter defines __call__, so an instance can be invoked with a name and return a greeting. callable(g) confirms that the invocation syntax is available. It does not tell us whether the call takes one argument, changes state, or could raise an exception; those details belong to the object's contract. Calling an object can look like calling a function, but an object may remember every

  2. 00:40

    call. We will use that property deliberately and then test whether two instances have separate memories. Before the first run, make a concrete prediction about the terminal output. A prediction turns a passive demonstration into a test: we can compare the actual result with the rule we thought the code followed. Keep the input small enough to inspect every value. The broken demo shares one global count between apparently separate counters. Two independent objects should not interfere. Give each instance its own value in __init__, then make __call__(step) validate the step, add it, and return the new value.

  3. 01:21

    For the counter project, parentheses should mean advance this counter by a positive step and return the new value. That is a small, precise interface. If a name like advance would be clearer to a reader, a normal method is also valid; __call__ is useful when an API expects a callable. The broken starter uses a module-level count. The output from b depends on what a did first, even though the objects look independent. That is a state ownership bug rather than a syntax error. Watch the output as evidence rather than jumping directly to a replacement snippet. The broken program may exit cleanly while still violating its contract.

  4. 02:03

    We will name the exact input, the observed result, and the result we wanted before changing code. Use callable(instance) to ask whether an object can be invoked. That check does not guarantee what arguments it accepts or what side effects occur. A callable object can be passed to APIs expecting a function, but its state makes call order meaningful. Document that state and avoid sharing one instance across concurrent operations without synchronization. The second example calls a twice and b once. The output 1 2 1 proves that each Tick instance has its own n value. A module-level global would instead create one shared sequence, causing b to start

  5. 02:46

    where a left off. Tests should create two instances specifically to catch that class of bug. The counter's initial value should be stored on self during __init__. Each call validates its step, updates self.value, and returns the result. State makes order matter, so avoid assuming a callable object behaves exactly like a pure function with the same arguments every time. The diagnostic Greeter proves that __call__ is enough for parentheses syntax. The Tick example then gives each instance a local counter. Look at the third number: it returns to one for the fresh object. The intermediate run isolates one mechanism.

  6. 03:29

    It is not the entire solution, and that is intentional. A small diagnostic is easier to explain than a large corrected application because it shows which assumption failed and which Python behavior we can rely on. Remove the global state. Counter(start=0) should keep its own value and __call__(step=1) should add a positive non-boolean integer step and return the new value. The prepared run must print 1 2 1. Invalid steps must raise ValueError without changing value. Update self.value, not a module-level count. Check type(step) is int and step > 0 before changing the instance.

  7. 04:11

    The last example shows why type(step) is int excludes True, even though isinstance(True, int) would pass. The task's count increment must be positive and genuinely numeric. Zero, negatives, floats, and booleans should all raise ValueError and leave value unchanged. The corrected Counter stores value on self and validates the step before addition. That makes it suitable as a callback with a simple contract. callable(counter) only promises the syntax works, not that any argument will be accepted. After the fix, compare the observed output with the original prediction. The code shown on screen is executed locally and the terminal pane replays that

  8. 04:52

    captured run. The linked lesson gives you a separate checked task, so you can apply the idea rather than only copy a slide. The checked counter accepts positive non- boolean integer steps and leaves its value unchanged on invalid calls. The final run compares two instances, calls one twice, and verifies the other remains at its start. This lesson distinguishes a callable interface from a stateless function. A stateful callable can be convenient as a callback, but sharing it across threads or asynchronous workers may require synchronization. This lesson keeps all calls sequential and focuses on observable state and

  9. 05:33

    validation. Once the local contract passes, the object can be used wherever that simple callable signature fits. Instance attributes keep state with the specific object. Rejected calls should leave the current value unchanged. Use one configured start, a step of three, and an invalid boolean. The invalid call must leave value at eight. Stateful callables are convenient, but their order dependence should be documented. An edge case is more valuable than repeating the same happy path. The final run challenges a boundary or invalid input and explains the decision the function makes.

  10. 06:14

    If your own implementation disagrees, use the lesson console to narrow the smallest failing case.

Understand the concept

A function can hold state in a closure, but a small class makes that state inspectable. Defining __call__ lets an instance respond to parentheses like a function. A counter can remember its current number between invocations while still exposing named methods and attributes.

The broken demo shares one global count between apparently separate counters. Two independent objects should not interfere. Give each instance its own value in __init__, then make __call__(step) validate the step, add it, and return the new value.

Use callable(instance) to ask whether an object can be invoked. That check does not guarantee what arguments it accepts or what side effects occur. A callable object can be passed to APIs expecting a function, but its state makes call order meaningful. Document that state and avoid sharing one instance across concurrent operations without synchronization.

The checked counter accepts positive non-boolean integer steps and leaves its value unchanged on invalid calls. The final run compares two instances, calls one twice, and verifies the other remains at its start. This lesson distinguishes a callable interface from a stateless function.

  • __call__ provides the invocation syntax
  • Instance state belongs to each object
  • Validate before mutating
  • callable() does not describe semantics

See it step by step

Read the code, predict the output, then compare it with the result.

01. Make an instance callable

PYTHON
class Greeter:
    def __call__(self, name): return 'Hi '+name
g=Greeter()
print(callable(g))
print(g('Ada'))
EXPECTED OUTPUT
True
Hi Ada

The instance can be invoked with ordinary call syntax.

02. Keep state on self

PYTHON
class Tick:
    def __init__(self): self.n=0
    def __call__(self):
        self.n+=1
        return self.n
a=Tick(); b=Tick()
print(a(),a(),b())
EXPECTED OUTPUT
1 2 1

Separate instances do not share a counter.

03. Check before updating

PYTHON
def valid_step(step): return type(step) is int and step>0
print(valid_step(2))
print(valid_step(True))
EXPECTED OUTPUT
True
False

Exact integer checking excludes booleans.

A closer look

Follow the reasoning, inspect each result, then try the suggested changes in the console below.

01 / 03

Give an object function-call syntax

Greeter defines __call__, so an instance can be invoked with a name and return a greeting. callable(g) confirms that the invocation syntax is available. It does not tell us whether the call takes one argument, changes state, or could raise an exception; those details belong to the object's contract.

For the counter project, parentheses should mean advance this counter by a positive step and return the new value. That is a small, precise interface. If a name like advance would be clearer to a reader, a normal method is also valid; __call__ is useful when an API expects a callable.

PYTHON
class Greeter:
    def __call__(self, name): return 'Hi '+name
g=Greeter()
print(callable(g))
print(g('Ada'))
EXPECTED OUTPUT
True
Hi Ada
Follow the reasoning
  1. Instantiate a class with __call__.
  2. Use callable and invoke it.
  3. Describe the counter's call contract.
02 / 03

Compare two independent instances

The second example calls a twice and b once. The output 1 2 1 proves that each Tick instance has its own n value. A module-level global would instead create one shared sequence, causing b to start where a left off. Tests should create two instances specifically to catch that class of bug.

The counter's initial value should be stored on self during __init__. Each call validates its step, updates self.value, and returns the result. State makes order matter, so avoid assuming a callable object behaves exactly like a pure function with the same arguments every time.

PYTHON
class Tick:
    def __init__(self): self.n=0
    def __call__(self):
        self.n+=1
        return self.n
a=Tick(); b=Tick()
print(a(),a(),b())
EXPECTED OUTPUT
1 2 1
Follow the reasoning
  1. Call the first instance twice.
  2. Call a fresh second instance once.
  3. Check both objects' stored values.
03 / 03

Reject invalid steps before mutation

The last example shows why type(step) is int excludes True, even though isinstance(True, int) would pass. The task's count increment must be positive and genuinely numeric. Zero, negatives, floats, and booleans should all raise ValueError and leave value unchanged.

A stateful callable can be convenient as a callback, but sharing it across threads or asynchronous workers may require synchronization. This lesson keeps all calls sequential and focuses on observable state and validation. Once the local contract passes, the object can be used wherever that simple callable signature fits.

PYTHON
def valid_step(step): return type(step) is int and step>0
print(valid_step(2))
print(valid_step(True))
EXPECTED OUTPUT
True
False
Follow the reasoning
  1. Check exact integer type and positive range.
  2. Snapshot value before a rejected call.
  3. Verify the value has not changed afterward.

Try it in Python

Edit the example and run it. Python starts in your browser the first time you click Run.

Python console

Ready to run
Need input()? Add one value per line
Ctrl / ⌘ + Enter to run
OUTPUT
Your output appears here.
Your turnRemove the global state. Counter(start=0) should keep its own value and __call__(step=1) should add a positive non-boolean integer step and return the new value. The prepared run must print 1 2 1. Invalid steps must raise ValueError without changing value.
Need a hint?

Update self.value, not a module-level count. Check type(step) is int and step > 0 before changing the instance.

Complete the task and select Check task to verify your code.

    Quick quiz

    Three questions. You can change your answers and try again.

    01.Which method makes an instance callable?

    02.Where should independent counters store their values?

    03.Does callable(obj) prove a call is safe?

    Typical mistakes

    Everyone meets these errors. See what causes them and how to fix them.

    Shared global count

    COMMON MISTAKE
    global count; count += step
    FIX
    self.value += step

    What happens: Separate counters affect one another.
    Instance attributes keep state with the specific object.

    Mutation before validation

    COMMON MISTAKE
    self.value += step; validate(step)
    FIX
    Validate, then update self.value

    What happens: An invalid step can corrupt state.
    Rejected calls should leave the current value unchanged.

    Browse this lesson’s problem fixes

    Sources and further reading