🧪 Test-Double Lab
Real file, fake file, flaky file, slow file — four completely different implementations, no shared base class.
Yet your app under test calls exactly the same read() and write() methods on every one of them.
This is duck typing in action: if it has read() and write(), Python treats it as a file — no inheritance, no isinstance, no registry. In this lab you will swap backends in one line, watch your app keep working, and learn why test doubles are the killer app for duck typing.
By the end you can…
- Explain duck typing in one sentence
- Read any program on this page line by line
- Distinguish dummies, stubs, spies, fakes and mocks
- Write a file-like object from scratch
- Inject a dependency three different ways
- Test I/O code without touching disk
- Write a contract test that catches lying fakes
- Know when duck typing is the right tool
You should already know
- How to run a Python function
- What a test is (roughly)
Classes, self, dictionaries and try/except are covered in the primer.
New to polymorphism? Read the Polymorphism Guide first.
Swap the Backend, Keep the App
Your app is the same in every run — it opens a file, writes a greeting, reads it back. Only the backend changes. Watch the trace panel: every action is a call to .write() or .read(), and each backend handles those calls completely differently.
Try this experiment (2 minutes): ① Run the app with each of the four backends and compare the trace. ② Switch to Flaky and press Run several times — the same code sometimes succeeds, sometimes raises IOError. ③ Switch to Slow and watch the app finish instantly anyway, because your test doesn't wait. ④ Add a new file to the fake backend and confirm the app reads it.
Backend inspector
Files in this backend
App under test
def greet(backend, name):
backend.write("greeting.txt", f"Hello, {name}!")
return backend.read("greeting.txt")The app has no idea which backend it is talking to. It just needs a write() and a read().
What to notice: the app calls backend.write() and backend.read() — no base class, no isinstance, no registration. Each backend decides what those methods mean: FakeFile keeps a dict, FlakyFile randomly raises, SlowFile is synchronous but logged as slow, RealFile would talk to disk. That is duck typing.
📖 Read the app under test, line by line
Honest note: a web page cannot write to your real disk, so the lab’s RealFile is simulated in JavaScript and says so in its trace. The Python programs further down use Pyodide, which provides a real (in-memory) file system, so there RealFile genuinely opens and writes files.
Five Ideas Behind Every Line on This Page
Every program here is built from the same few Python ideas. If any of them feels shaky, the short programs in this section will fix that in a few minutes. If they feel familiar, skim the cards and move on to the idea.
How to study with this page. ① Predict — open “Predict first” and guess the output before you press Run. ② Run the program. ③ Read the line-by-line explanation: tap any line, or switch on Step mode and press Next to walk through the program one line at a time while the matching line lights up in the code. ④ Change one thing in the playground and predict again. Prediction before running is the habit that turns reading into learning.
1. Objects & methods
A class is a blueprint, an object is one thing built from it, and a method is a function that belongs to the class. self is “the object I was called on”.
2. Attribute lookup
obj.name means “search obj, then its class, then its parents for name”. It happens when the line runs, not before.
3. Dictionaries
Key → value storage. d[k] raises KeyError if missing, d.get(k, default) never raises, k in d tests presence.
4. Exceptions
Errors are objects that travel up the call stack until a matching except catches them. raise throws one on purpose.
5. Everything is an object
Functions, classes and methods are values too. You can pass them around, store them and call them later.
🦆 Program A — Two unrelated classes, one function
This is duck typing in its smallest possible form. Duck and Robot share nothing, yet make_noise works with both. Notice that the function never mentions either class.
Takeaway: make_noise needs only something with a speak() method. Replace “speak” with “write” and “read” and you have the whole lab.
🗂️ Program B — Dictionaries, exceptions and f-strings
These three tools build every fake file in the lab. The dictionary is the pretend disk, the exception is how a fake reports failure, and the f-string builds the greeting.
Watch this distinction: a missing dictionary key raises KeyError, but a missing file raises FileNotFoundError. A good fake converts its internal failure into the exception the real object would raise, so the app handles both identically.
📖 A tiny Python reading glossary
| You see | Read it as |
|---|---|
def f(a, b=2): | Define a function; b is optional and defaults to 2. |
self.x = 1 | Store 1 on this object under the name x. |
class B(A): | B inherits everything from A and may override parts. |
super().m() | Call the parent class’s version of method m. |
with open(p) as f: | Open a file and close it automatically at the end of the block. |
try: … except E as e: | Run the block; if error type E occurs, jump to the handler and call the error e. |
x if c else y | Use x when condition c is true, otherwise y. |
[f(c) for c in items] | Build a new list by applying f to every item. |
... | “Nothing here yet” placeholder (the Ellipsis object). |
What Is Duck Typing, Really?
"If it walks like a duck and quacks like a duck, it is a duck." In Python, an object is judged by the methods it has, not by what it inherits from. Your app doesn't care that FakeFile is unrelated to RealFile — it cares that both respond to read() and write().
🔌 Real-world analogy: the USB port
A USB port doesn't know or care if you plug in a mouse, a keyboard, a hard drive or a fan. As long as the device speaks USB, it works. Your app is the port; every file backend is a device. Test doubles exist precisely because the port is so easy to plug things into — you can substitute a fake for the real thing and the app never notices.
The contract: a file backend must provide write(name, text) and read(name). That's it. No base class, no typing.Protocol required — though both are useful when you want the promise written down.
The four kinds of test double
Stub
Returns canned data, ignores input. Fast, deterministic, no side effects.
class Stub: def read(...): return "..."Spy
Like a stub, but records every call so you can assert on it after.
self.calls = []Fake
A working implementation with a shortcut — an in-memory database, a dict instead of a file.
self.store = {}Mock
A stub with expectations baked in — fails if not called correctly.
assert called_once_with(...)All four work the same way from the app's point of view: they expose the methods the app calls. Which one you reach for depends on what you want to test.
Why bother? Three payoffs
- Fast tests. An in-memory fake runs in microseconds. Disk, network and database calls take milliseconds to seconds.
- Deterministic tests. A stub always returns the same thing. A real disk can be full, a network can be down, a clock can roll over.
- Testable error paths. Hard to make a real disk fail on demand; trivial to make
FlakyFileraise.
The rule that keeps this honest: a test double must respect the real object's contract. If RealFile.read raises FileNotFoundError for a missing file, FakeFile must too — otherwise your tests pass while production breaks.
🕰️ A short history of the idea
The phrase “duck test” (if it walks and quacks like a duck, it is a duck) is old and often attributed to the poet James Whitcomb Riley. Programmers borrowed it for a very practical question: how do you decide whether an object is usable? Two answers compete.
- Nominal typing — an object is usable if it declares membership: it inherits from the right class, or is registered with it. Java and C# work this way, and in Python so do abstract base classes (
abc, introduced by PEP 3119). - Structural typing — an object is usable if it has the right shape: the methods and attributes the caller needs. Plain Python behaves this way at runtime, and
typing.Protocol(PEP 544, Python 3.8) brings the same idea to static type checkers.
The official Python glossary defines duck-typing as exactly this: judge an object by its methods and attributes rather than its type, and avoid tests such as isinstance() where possible. The test-double vocabulary comes from a different tradition: Gerard Meszaros introduced the term test double in his book xUnit Test Patterns (2007), borrowing from the stunt double in films.
🧪 The full family of test doubles
Earlier we met four kinds. Meszaros’s catalogue has five; the fifth, the dummy, is the simplest of all. Use this table as a field guide.
| Double | Has real behaviour? | Records calls? | Fails the test itself? | Use it when… |
|---|---|---|---|---|
| Dummy | No | No | No | A parameter must be filled but is never used, e.g. greet(None, ...) in a branch that never touches the backend. |
| Stub | Canned answers only | No | No | You need one specific answer: “pretend the file contains X”. |
| Spy | Optional | Yes | No (you assert afterwards) | You must prove the app did something: sent, wrote, retried. |
| Fake | Yes, simplified | Optional | No | The app needs a working substitute: dict instead of disk, SQLite in memory. |
| Mock | Scripted | Yes | Yes (expectations built in) | The interaction itself is the requirement, and you want the test to fail loudly if it differs. |
Everyday speech often says “mock” for all five. That is common, but knowing the differences helps you choose the cheapest double that proves what you need.
⚖️ Two styles of checking: state vs behaviour
Martin Fowler’s essay Mocks Aren’t Stubs describes two schools of test. State verification runs the code and then inspects results: “the greeting returned was Hello, Ana!”. Behaviour verification checks the conversation: “write was called once, then read”. Neither is better in general.
- Prefer state checks when the result is observable — they survive refactoring because they ignore how the code works.
- Prefer behaviour checks when the side effect is the product: an email sent, a payment requested, a retry attempted.
- Fakes lean towards state checks; spies and mocks lean towards behaviour checks.
🏛️ Three principles hiding underneath
- Dependency injection. Code that receives its collaborators can be given a different one.
greet(backend, name)is the smallest example imaginable. - Behavioural substitutability (the Liskov idea). A double can stand in for the real object only if it keeps the same promises: same method names, same parameters, same return types, same exceptions in the same situations. This is the formal version of “mirror the contract”. Duck typing gives you no inheritance to enforce it, so tests must.
- Seams. Michael Feathers calls a place where you can change behaviour without editing the code a seam. The
backendparameter is a seam: the app’s source never changes, only what is plugged into it.
Why this matters beyond tests: the same seam lets you add logging, retries, caching, encryption or a cloud backend later, without touching the app. Testing is just the first customer.
From "impossible to test" to "swap in one line"
Press Run on every step, then read the line-by-line explanation under it. Each block is self-contained, so you can also paste it into the playground and change it.
Start with code that touches the disk
This is what most beginners write. It works, but the test has to touch a real file. Every run leaves a greeting.txt behind, and a full disk or a permission error breaks the test suite.
The smell: tests that touch the real disk are slow, flaky and leave debris. What we want is the same function, tested against something that behaves like a file but doesn't need one.
Pass the backend in
The fix is a one-line change to the app: accept backend as a parameter. Now the caller decides what kind of file it gets.
Build a fake — no inheritance required
Notice FakeFile does not inherit from anything. It just has the methods. That's duck typing.
Milestone reached. You replaced disk I/O with a dict. Tests are now instant and leave no files behind. The app itself didn't change between steps 2 and 3 — only the object passed in.
Build a spy — record every call
A spy is a fake that also records its history, so the test can verify what the app did — not just what it returned.
Add a failing backend to test the error path
How would you test that the app handles a disk error? With a double that always raises. You can't easily do that with a real file, but you can with a ten-line class.
Milestone reached. You just tested a branch — the error path — that is essentially untestable against a real file. This is why duck typing and testing go together.
The Whole Lab in One File
This is the exact model behind the live lab above — the same greet(), four backends, no shared base class. Press Run to execute it in your browser with Pyodide, then work through every line in the explanation below it.
test_double_lab.py
Why this works: greet() appears once and calls backend.write() / backend.read(). Each backend implements those methods differently. FlakyFile inherits from FakeFile but overrides write — polymorphism with a twist. RealFile shares no parent at all — pure duck typing.
🔍 Code-reading guide
FakeFile— a dict, plus the two methods. That's a fake.FlakyFile(FakeFile)— inherits the methods, overrideswriteto randomly raise.super().write()delegates the successful branch to the parent.SlowFile— same idea, but tracks a call counter. Real-world SlowFile wouldtime.sleep(1)— the point is that in the test we can skip the wait entirely.RealFile— no parent at all. Yet the samegreet()accepts it, because it exposes the same two methods.greet()— the app. Not a singleisinstance, not a singlehasattr. It just calls the methods and trusts the caller to pass a conforming object.- The final loop —
hasattrverifies that all four backends satisfy the contract, without any of them sharing a base class.
How Duck Typing Works Under the Hood
Attribute lookup in one table
When your app calls backend.write("x", "y"), Python performs two lookups — one for write on the object, one for the result of calling it with those arguments. Here is what it finds per backend:
| Object | Resolves to | Behaviour |
|---|---|---|
FakeFile | FakeFile.write | Stores text in a dict. |
FlakyFile | FlakyFile.write | Maybe raises IOError, else delegates to FakeFile.write. |
SlowFile | FakeFile.write (inherited) | Behaves like FakeFile for writes; overrides read only. |
RealFile | RealFile.write | Writes to the filesystem. |
A random object() | not found | Raises AttributeError: 'object' object has no attribute 'write' |
Notice that SlowFile inherits write from FakeFile — that's method lookup going up the MRO. But RealFile has no parent, so its write must come from the class itself. Both are found the same way: an attribute lookup on the instance.
Protocol: writing the contract down
Duck typing is great at runtime, but it fails late — if the caller passes the wrong object, the error appears on the first method call inside the app. typing.Protocol catches that earlier, at type-check time, without forcing inheritance.
You already use this daily. print(x, file=f) works with any object that has .write() — a real file, io.StringIO, or your own class. That's duck typing in the standard library.
EAFP vs LBYL
Two styles of handling "it might not work":
| Style | Meaning | Example |
|---|---|---|
| LBYL | Look Before You Leap — check first | if name in backend.store: backend.read(name) |
| EAFP | Easier to Ask Forgiveness than Permission — try it, catch the error | try: backend.read(name) except FileNotFoundError: ... |
Duck typing leans EAFP. You don't check that write exists — you call it and catch AttributeError at the outer boundary if you must. But wrap small blocks, not whole functions: a broad except AttributeError will also hide typos inside the method body.
Mocking with unittest.mock
Python ships a built-in Mock class that auto-generates methods. It is the fastest way to get a stub, but it has no idea what the real object does — so use it when you want a spy, and use a hand-written class when you want realistic behavior.
Side-by-side: strict typing vs duck typing
🔬 See the lookup yourself
The table above is a summary. This program lets you observe the same facts: the class search order, inherited versus overridden methods, bound methods, and the exact error a wrong guess produces.
Key insight: methods live in classes, data lives in the object’s __dict__, and a lookup walks the chain from the object to its class to the parents. Duck typing works because this search happens at the moment of the call, using only the name.
⚔️ Three ways to describe a contract
| Plain duck typing | Protocol | Abstract base class | |
|---|---|---|---|
| Must inherit? | No | No | Yes (or register) |
| Contract written down? | Only in docs and tests | Yes, in code | Yes, in code |
| Wrong object detected | At the call, at runtime | Before running, by a type checker | At creation of the object, at runtime |
| Works with third-party classes? | Yes | Yes | Only if registered |
| Best for | Small scripts, quick doubles | Libraries and larger codebases with type checking | When you also want to share code through the parent |
When duck typing is the wrong tool: when a wrong object would fail silently or far from the cause (for instance data gets written and corrupted before the failing call), or when many teams depend on the contract and need it enforced. Add a Protocol or a contract test (next section) — do not abandon duck typing, just write the promise down.
From Toy Examples to Professional Habits
The earlier sections taught the mechanism. This section shows the six habits that experienced Python developers build on top of it. Each has a runnable program with a full line-by-line explanation.
1 · Three ways to inject a backend
Passing the dependency as a function parameter is only one option. Classes usually take it in the constructor, and long-lived objects often accept an optional one with a sensible default.
| Technique | Use when | Trade-off |
|---|---|---|
| Parameter | A single function needs it | You pass it on every call. |
| Constructor | Several methods share it | The object is tied to one backend for its life. |
| Default argument | Production should work with zero setup | The default can hide a real dependency, so test code must remember to override it. |
2 · Injecting time: making slow things fast
Waiting is a dependency like any other. If the delay is a parameter, the test can replace “wait two seconds” with “write the number down”. The same trick works for clocks (now=datetime.now) and random numbers.
Duck typing is not only for objects. The “sleep” here is anything callable with one number: time.sleep, a lambda, or list.append.
3 · Composing backends like Lego
Since every backend promises the same two methods, a backend can hold another backend and add behaviour around it: logging, retrying, caching, encryption, metrics. The app still sees one object. This is the Decorator (or wrapper) pattern, and duck typing makes it almost effortless.
4 · A real test with unittest
Python ships with a test framework. This program defines three tests: the happy path, a state change and an error path, all against fakes, and runs them in the browser.
Rule of thumb for good tests: one behaviour per test, a name that reads like a sentence, a fresh double for every test, and no test that depends on another.
5 · The same test with pytest
Most professional projects use pytest. Its fixtures and plain assert statements remove boilerplate, and a parametrised fixture can run every test against several backends.
6 · Contract tests: stop fakes from lying
The biggest risk of any test double is that it drifts away from the real thing. A contract test is a single checklist run against every implementation. If the real file passes and the fake fails, the fake is wrong — and you find out in a second, not in production.
Make it a habit: whenever you write a new double, add its class to the contract test’s list. One extra line protects every test that uses it.
🧭 Which double should I write?
- Does the code under test ignore this dependency? Pass a dummy (
Noneorobject()). - Do I need only one canned answer? Write a stub, or
Mock(return_value=...). - Do I need a working substitute? Write a fake and put it under a contract test.
- Must I prove a call happened? Add recording (spy) or use
Mock.assert_called_once_with. - Do I need to cause failures or delays on demand? Write a tiny special-purpose double or a wrapper like
RetryBackend.
When two answers fit, choose the simplest one. A stub that is five lines long is easier to trust than a fake that is fifty.
Four Classic Test-Double Mistakes
The fastest way to understand the rules is to watch them fail. Run each broken example, read the error, then run the fix.
Mistake 1 — A fake that lies about the real contract
The real backend raises on a missing file. The fake returns an empty string — so the app "works" in tests but crashes in production.
Mistake 2 — Wrapping too much in except AttributeError
A broad exception handler hides both "the object has no method" and "the method has a typo inside it".
Mistake 3 — Testing the double instead of the app
If the test asserts on how the fake stored its data, you're testing the fake, not the behaviour. Change the fake's internals — the test should still pass.
Mistake 4 — Forgetting to clean up after a real backend
Real file tests that don't clean up leave debris. Your next run may behave differently because the file already exists.
🩺 Error-message decoder
Duck typing shows its mistakes as ordinary Python exceptions. Learn to read the message and you can usually fix the problem in seconds.
| Message (shortened) | Most likely cause | Fix |
|---|---|---|
AttributeError: 'dict' object has no attribute 'write' | You passed the wrong kind of object, or misspelled the method name. | Check what you pass to the app; compare method names with the contract. |
AttributeError: 'FakeFile' object has no attribute 'store' | A subclass defined __init__ but forgot super().__init__(). | Call the parent initialiser first. |
TypeError: write() takes 2 positional arguments but 3 were given | A method was written without self. | The first parameter of every method must be self. |
TypeError: write() missing 1 required positional argument: 'text' | The double’s signature differs from the real one, or the caller passed too few arguments. | Make the parameter lists identical. |
KeyError: 'greeting.txt' | A fake leaks its dictionary’s error instead of the contract’s. | Catch it or check in and raise FileNotFoundError. |
FileNotFoundError: [Errno 2] No such file or directory | The real file was missing (correct behaviour!). | Make sure your fake raises the same exception in the same situation. |
read() returns None | A method computes a value but forgets return. | Add return. |
AssertionError | A check failed — the code or the expectation is wrong. | Print both values; decide which one is correct. |
Debug habit: when a test fails, first check whether the double matches the real object's contract. Nine times out of ten, the fake lies.
Four Challenges and a Playground
Try each challenge in the playground below before opening the hint or the solution.
Easy Challenge 1 — A CountingFile spy
Write a CountingFile that behaves like FakeFile but counts how many times read and write are called. Then verify that greet() calls each exactly once.
💡 Hint
You can subclass FakeFile and add self.writes = 0 / self.reads = 0 counters, or start from scratch. Either way the methods still take (self, name, text) and (self, name).
✅ Solution
Medium Challenge 2 — A double that rejects oversized writes
Create StrictFile that raises ValueError if the text is longer than 20 characters. Use it to test that greet() fails loudly for very long names, then write a version of greet() that truncates the name so the test passes.
💡 Hint
The strict backend behaves like FakeFile except write checks len(text) > 20. Wrap the app call in try/except ValueError.
✅ Solution
Medium Challenge 3 — A retry backend
Real I/O is sometimes flaky. Wrap a backend in a RetryBackend that tries the operation up to three times before giving up. Test it with a fake that fails twice and succeeds the third time.
💡 Hint
RetryBackend holds the wrapped backend and forwards read/write with a for _ in range(3): try: return ... except IOError: pass loop. The test fake can track a counter.
✅ Solution
Hard Challenge 4 — Convert the app to use a Protocol
Take the greet() app and add a FileBackend Protocol that describes the two methods. Then have a type checker confirm that both FakeFile and RealFile satisfy it, even though they share no parent.
💡 Hint
Import Protocol from typing and annotate both arguments. You can't run a type checker inside Pyodide, but you can print isinstance(fake, FileBackend) if you also mark the protocol @runtime_checkable.
✅ Solution
🧪 Playground — write your own code
Edit freely, then press Run. Starter code is below; solve the challenges here. Press Ctrl/⌘ + Enter to run quickly.
📖 Understand the starter code before you change it
Check Your Understanding
Twelve questions. You get instant feedback and an explanation after each answer. Your best score is saved in this browser.
Key Takeaways
- Duck typing judges an object by its methods, not its class ancestry.
- Test doubles (stubs, spies, fakes, mocks) plug into the same slot as the real object if they honor the contract.
- Pass the backend in — one small change to the app buys infinite testability.
- Mirror the contract: if the real backend raises, the fake must raise too.
- Use
Protocolwhen you want the contract written down and checked by a type checker. - Wrap small blocks in
try/except— a broadexcept AttributeErrorhides real bugs. - Inject everything slow or random — backends, clocks, sleeps — so tests control them.
- Run a contract test over the real object and every fake so doubles cannot drift.
- Choose the simplest double that proves what you need: dummy, stub, spy, fake, then mock.
Where You See This in Real Software
Unit tests
Every network call, database query and clock read is stubbed so tests run in milliseconds.
File-like objects
io.StringIO, BytesIO, network sockets — all have .read() / .write().
In-memory databases
SQLite's :memory: mode is a duck-typed double for a real database connection.
Plugin systems
Anything with .load() and .run() counts as a plugin — no registration required.
Glossary
Judging an object by the methods it has, not by its class.
Any object used in place of a real dependency during a test.
A double that returns canned data.
A stub that records calls so you can assert on them.
A working implementation with a shortcut (dict instead of disk).
A double with expectations baked in.
The set of methods and behaviours a caller relies on.
A type-hint-only interface matched by shape, not inheritance.
Easier to Ask Forgiveness than Permission — try, then catch.
Look Before You Leap — check first, then act.
A placeholder passed only to fill a parameter; never actually used.
Handing an object its collaborators instead of letting it create them.
A place where behaviour can change without editing the code around it.
Method resolution order: the list of classes Python searches for an attribute.
A method tied to one object, so self is supplied automatically.
An object that holds another with the same interface and adds behaviour.
One checklist run against every implementation of an interface.
Reusable test setup, e.g. a pytest function that builds a backend.
Compatibility by declared ancestry versus by matching shape.
Where to Go Next: Three Build Projects
Reading is not enough — build something. Each project reuses everything on this page and is small enough for an evening.
Notes app with swappable storage
Write a Notebook that saves and loads notes through a backend. Provide a dict-backed fake, a real-file backend and a JSON backend, then run one contract test over all three.
Resilient uploader
Build RetryBackend with exponential waiting (inject the sleep!), plus a LoggingFile. Prove with a deterministic flaky double that three failures lead to a fourth, successful attempt.
Cache decorator with a clock
Wrap any backend in a CachingFile that expires entries after N seconds. Inject a fake clock so a test can “travel” an hour in microseconds, and add a Protocol for both the backend and the clock.
Further Reading
- Python glossary: duck-typing — the official one-paragraph definition.
- unittest.mock — the standard library’s mock objects,
patchand assertions. - typing.Protocol — structural subtyping in the type system.
- PEP 544 — Protocols — the design document behind static duck typing.
- Mocks Aren’t Stubs — Martin Fowler on state versus behaviour verification.
- pytest documentation — fixtures, parametrisation,
tmp_pathandmonkeypatch.
Frequently Asked Questions
Is duck typing the same as polymorphism?
Not quite. Polymorphism is the idea of "one interface, many implementations." Duck typing is one way to achieve it in Python — the version that doesn't need a shared parent class. Method overriding via inheritance is another way.
When should I use unittest.mock.Mock vs a hand-written fake?
Use Mock when you just need a spy — a quick placeholder that records calls. Use a hand-written fake when you need realistic behavior (a working dict-backed file, an in-memory database). Hand-written fakes respect the real contract; mocks won't stop you from writing a fake that lies.
Does duck typing make bugs harder to find?
It can — a wrong object surfaces its error on the first method call, not at the declaration. Two mitigations: (1) add a typing.Protocol so the type checker catches it; (2) keep the boundary small — pass one backend in, don't forward it through ten layers.
Why is FakeFile an acceptable test double but Mock() can also be one?
Because the app only checks for methods — that's duck typing. Anything with write() and read() is accepted, whether it's a real class, a fake, a spy or a Mock. The choice is about what you want to verify, not whether the app will accept it.
What if my test needs to check write() was called with specific arguments?
That's what a spy is for. Use Spy from challenge 1 or unittest.mock.Mock: mock.write.assert_called_once_with("greeting.txt", "Hello, Ana!"). But resist the urge to assert on internals — most tests should assert on what the app returned, not on how many times a private method was called.
Why does my fake need super().__init__()?
When a subclass defines its own __init__, Python no longer runs the parent’s automatically. If the parent’s initialiser creates self.store and you skip it, the first write fails with AttributeError: ... no attribute 'store'. Call super().__init__() as the first line of your own initialiser.
Is it wrong to use isinstance in Python?
No — it is sometimes the right tool, for example when the code genuinely behaves differently for strings and lists. The guideline is narrower: do not use isinstance to gatekeep dependencies that only need a couple of methods, because that blocks perfectly good substitutes such as test doubles.
Should I test my fakes?
Yes, but cheaply: one contract test that runs against the real implementation and every fake. If a fake and the real thing ever disagree, the contract test shows it immediately. You do not need separate unit tests for each method of each fake.
How is a fake different from a mock in practice?
A fake works: write then read returns your text, like a small real file. A mock only scripts answers and checks calls. With a fake, assertions look at results; with a mock they look at calls. If the app logic depends on remembered data, a fake is usually less brittle.
Can I use a dictionary directly as a backend?
Only if the app calls dictionary-style operations. greet calls .write() and .read(), which dictionaries do not have, so a plain dict fails with AttributeError (the lab’s “Break the contract” button shows exactly that). You would write a small adapter class around the dictionary.
Where should I go next?
Try Currency Converter (operator overloading) for a different angle on polymorphism, or Shape Renderer (method overriding with an abstract base). The full guide at polymorphism.html covers all four flavors.
10 Live Projects
You're looking at Project 06. Here are the other nine — each demonstrates a different flavor of polymorphism.
| # | Project | Flavor |
|---|---|---|
| 01 | 🔷 Shape Renderer | Method overriding |
| 02 | 🖼️ Image Filter Plugin | Duck typing |
| 03 | 💱 Currency Converter | Operator overloading |
| 04 | 🍳 Recipe Formatter | Single dispatch |
| 05 | 🎧 Media Player | Method overriding |
| 06 | 🧪 Test-Double Lab | Duck typing ← you are here |
| 07 | 📒 Double-Entry Ledger | Operator overloading |
| 08 | 📨 Log Router | Single dispatch |
| 09 | ♞ Chess Pieces | Method overriding |
| 10 | ✅ Protocol Checker | Duck typing |