When does unittest.mock.create_autospec catch a bug that Mock(spec=...) misses?
answer
- One fence is shallow, the other is not
- Names alone versus how you call it
- Signatures introspected from the real object
- Recursive: children are specced too
- Blind to attributes set in __init__
basics
~20 screate_autospec rebuilds the target recursively and gives every callable its real signature, so a wrong number or name of arguments raises TypeError at the call. Mock(spec=...) checks attribute names only and leaves its children unconstrained.
solid answer
~50 s`spec=` is a shallow, name-level fence: it rejects unknown attribute names and stops there, so `Mock(spec=Ledger).post(1, 2, 3, 4)` passes even though the real method takes two arguments, and the mock's children are plain unconstrained mocks. `unittest.mock.create_autospec` walks the object, specs every attribute from its real counterpart, and builds each callable from `inspect.signature`, so arity and keyword mistakes raise `TypeError` at the call site with messages like *missing a required argument*. Autospeccing a class gives a callable whose `return_value` is an instance-specced non-callable mock; pass `instance=True` when the code under test receives an already-built object. `async def` members come back as `AsyncMock`. `patch(target, autospec=True)` applies the same treatment in place. The limits: it sees only what exists on the object at spec time, so attributes assigned in `__init__` are missing, and it costs roughly an order of magnitude more to construct.
code
python · 14 linesfrom unittest.mock import Mock, create_autospec
class Ledger:
def post(self, amount, account): ...
Mock(spec=Ledger).post(1, 2, 3, 4) # spec= checks the NAME only: this passes
auto = create_autospec(Ledger, instance=True)
try:
auto.post(1, 2, 3, 4)
except TypeError as exc:
print(exc) # too many positional arguments
auto.post(100, "cash")
auto.post.assert_called_once_with(100, "cash")go deeper
Know that there are two strengths of double: one that only checks attribute names exist, and one built from the real object that also checks you call it with the right arguments. Reach for the stronger one when you have a real class to copy.
Explain what recursion and signature introspection buy you: children are specced too, arity and keyword errors raise TypeError at the call, and patch takes an autospec flag that does the same for a patched target.
Show judgment about the limits. An interviewer expects you to name what autospec cannot see — attributes created in init, dynamically served attributes, behavioural drift — and to say where you accept the construction cost and where you do not.
Own it as policy. Decide which boundaries in the system must be autospecced, which need real-collaborator coverage instead because interface fidelity is not behavioural fidelity, and how that is kept consistent as the suite and the team grow.
## Where `spec=` stops `Mock(spec=SomeClass)` fences a double at the *name* level: - it copies the attribute names from the object you hand it, - raises AttributeError for anything else, - and fakes `__class__` so isinstance checks pass. That is where it stops. The mock's children are ordinary unconstrained mocks, and no call is ever checked against a signature. So `Mock(spec=Ledger).post(1, 2, 3, 4)` sails through even though `Ledger.post` takes `(self, amount, account)`, and `specced.post.anything_at_all` is invented as usual one level down. ## What `create_autospec` adds `unittest.mock.create_autospec` does the thing people usually think `spec=` does. It walks the target recursively and rebuilds it: every attribute becomes a mock specced from the corresponding real attribute, and every callable is created with the **real signature**, introspected with `inspect.signature`. Calls are then bound against that signature before they are recorded, so passing too many positional arguments, omitting a required one, or using a keyword the function does not accept raises `TypeError: too many positional arguments` / `missing a required argument: 'account'` at the call site. Methods are built with `self` already bound, so you write the call exactly as production writes it. Because the rebuild is recursive, the constraint survives one level down — child attributes are specced too, not bare mocks. ## Classes, instances and coroutines Autospeccing a *class* gives you a callable mock whose `return_value` is a `NonCallableMagicMock` specced as an instance of that class, which mirrors reality: calling the class constructor yields an instance, and the instance itself must not be callable. - When the code under test receives an already-constructed collaborator, pass `instance=True` so the double is non-callable from the start and an accidental `collaborator()` raises TypeError instead of quietly returning a mock. - Members defined with `async def` come back as `AsyncMock`, so awaiting them works and the double does not need special handling. `patch(target, autospec=True)` applies the same treatment in place: the replacement is built from the object it is replacing, so an arity mistake in the test raises TypeError instead of being recorded as a call. For a plain function this replacement is a real function object with a matching signature that delegates to the mock, which is what makes the signature error come from argument binding rather than from a mock-specific check; you still get `assert_called_once_with` and the rest of the assertion surface on it. ## The limits The limits matter as much as the capability, and are where a senior answer separates itself. 1. First, autospec can only see what is on the object at spec time. Attributes assigned in `__init__` do not exist on the class, so `create_autospec(Ledger).cache` raises AttributeError even though every real instance has one. Anything served dynamically through a `__getattr__` hook is invisible for the same reason. The workarounds are to autospec a live *instance* rather than the class, to configure the missing attribute explicitly on the double, or to introduce a narrow explicit interface — a `typing.Protocol` or an abstract base — and spec against that. 2. Second, autospec is not free. Building one is roughly an order of magnitude more expensive than `Mock(spec=...)` because it introspects the whole object graph — measurable, if not usually decisive, in a suite already running 27 minutes for a payment reconciliation job. Autospec the collaborators that carry your real contracts; you do not need it for a throwaway double whose only job is to be passed through. 3. Third, and most important: autospec validates the shape of the interface *as it exists in this process*. It says nothing about whether the object you are mocking behaves as your test pretends, nothing about the return-value shape, and nothing about a version of the dependency other than the one installed. A mocked collaborator whose real counterpart changed behaviour without changing its signature will still pass. That is a property of doubles in general, not a defect of autospec, and it is why **interface-level fidelity** and **behavioural fidelity** are separate problems. ## Choosing the double Rule of thumb: - reach for autospec by default when the double stands in for something you own a contract with; - use `instance=True` when the code receives an instance rather than a factory; - fall back to `Mock(spec_set=...)` when you only need a name-level fence cheaply; - and reach for a hand-built mock only when the collaborator has no importable interface at all — in which case seal it rather than leaving it open.
- What do you get when create_autospec is given a class rather than an instance?A callable mock standing in for the class, whose `return_value` is a non-callable mock specced as an instance of it — mirroring the fact that calling a class yields an instance that must not itself be callable. Pass `instance=True` when the code under test receives an already-constructed collaborator, so that an accidental call on the double raises TypeError instead of quietly returning another mock.
- What can autospec still not see about the real object?Anything not present at spec time: attributes assigned in `__init__` when you spec the class, attributes served through a dynamic attribute hook, and anything monkeypatched on later. Accessing them raises AttributeError even though production has them. The fixes are to spec a live instance, configure the attribute explicitly, or introduce a narrow explicit interface such as a Protocol and spec against that.
- Is there a reason not to autospec everything?Two. Construction is roughly an order of magnitude more expensive than a plain specced mock because the whole object graph is introspected, which shows up in a large suite. And autospec only validates the interface as installed in this process — it says nothing about return-value shape or about a collaborator whose behaviour changed without its signature changing. Autospec the collaborators carrying real contracts; a pass-through double does not need it.
spec= checks that the words you say are in the dictionary; autospec also checks that the sentence parses.
saying these in an interview costs you the question
- Says spec= already checks call signatures
- Thinks autospec sees attributes assigned in __init__
- Believes autospec validates return values or types
- Thinks create_autospec on a class yields an instance mock
- Assumes autospec protects against a changed remote API
- Turns on autospec everywhere without noticing the construction cost