Guide
Project 06 · Duck Typing

🧪 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.

~2 hours Beginner → Intermediate 29 runnable programs 600+ lines explained 12-question quiz
Full Guide Open the Lab Build It Jump to Quiz

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.

Your progress (saved in this browser)0 / 10 sections complete

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: FakeFile App is backend-agnostic
choose backend →

Backend inspector

🧪
FakeFile
In-memory dict · instant · never fails

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().

Press Run greet() to see what happens.
Backend trace — every line is a call your app made:
Ready.

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 seeRead it as
def f(a, b=2):Define a function; b is optional and defaults to 2.
self.x = 1Store 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 yUse 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 FlakyFile raise.

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.

DoubleHas real behaviour?Records calls?Fails the test itself?Use it when…
DummyNoNoNoA parameter must be filled but is never used, e.g. greet(None, ...) in a branch that never touches the backend.
StubCanned answers onlyNoNoYou need one specific answer: “pretend the file contains X”.
SpyOptionalYesNo (you assert afterwards)You must prove the app did something: sent, wrote, retried.
FakeYes, simplifiedOptionalNoThe app needs a working substitute: dict instead of disk, SQLite in memory.
MockScriptedYesYes (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 backend parameter 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, overrides write to randomly raise. super().write() delegates the successful branch to the parent.
  • SlowFile — same idea, but tracks a call counter. Real-world SlowFile would time.sleep(1) — the point is that in the test we can skip the wait entirely.
  • RealFile — no parent at all. Yet the same greet() accepts it, because it exposes the same two methods.
  • greet() — the app. Not a single isinstance, not a single hasattr. It just calls the methods and trusts the caller to pass a conforming object.
  • The final loop — hasattr verifies 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:

ObjectResolves toBehaviour
FakeFileFakeFile.writeStores text in a dict.
FlakyFileFlakyFile.writeMaybe raises IOError, else delegates to FakeFile.write.
SlowFileFakeFile.write (inherited)Behaves like FakeFile for writes; overrides read only.
RealFileRealFile.writeWrites to the filesystem.
A random object()not foundRaises 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":

StyleMeaningExample
LBYLLook Before You Leap — check firstif name in backend.store: backend.read(name)
EAFPEasier to Ask Forgiveness than Permission — try it, catch the errortry: 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 typingProtocolAbstract base class
Must inherit?NoNoYes (or register)
Contract written down?Only in docs and testsYes, in codeYes, in code
Wrong object detectedAt the call, at runtimeBefore running, by a type checkerAt creation of the object, at runtime
Works with third-party classes?YesYesOnly if registered
Best forSmall scripts, quick doublesLibraries and larger codebases with type checkingWhen 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.

TechniqueUse whenTrade-off
ParameterA single function needs itYou pass it on every call.
ConstructorSeveral methods share itThe object is tied to one backend for its life.
Default argumentProduction should work with zero setupThe 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?

  1. Does the code under test ignore this dependency? Pass a dummy (None or object()).
  2. Do I need only one canned answer? Write a stub, or Mock(return_value=...).
  3. Do I need a working substitute? Write a fake and put it under a contract test.
  4. Must I prove a call happened? Add recording (spy) or use Mock.assert_called_once_with.
  5. 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.

✓ Fix: mirror the real contract

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".

✓ Fix: catch only what you intend

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.

✓ Fix: assert on what the app should return

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.

✓ Fix: tmp_path, try/finally, or a fake

🩺 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 causeFix
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 givenA 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 directoryThe real file was missing (correct behaviour!).Make sure your fake raises the same exception in the same situation.
read() returns NoneA method computes a value but forgets return.Add return.
AssertionErrorA 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 Protocol when you want the contract written down and checked by a type checker.
  • Wrap small blocks in try/except — a broad except AttributeError hides 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

Duck typing

Judging an object by the methods it has, not by its class.

Test double

Any object used in place of a real dependency during a test.

Stub

A double that returns canned data.

Spy

A stub that records calls so you can assert on them.

Fake

A working implementation with a shortcut (dict instead of disk).

Mock

A double with expectations baked in.

Contract

The set of methods and behaviours a caller relies on.

Protocol

A type-hint-only interface matched by shape, not inheritance.

EAFP

Easier to Ask Forgiveness than Permission — try, then catch.

LBYL

Look Before You Leap — check first, then act.

Dummy

A placeholder passed only to fill a parameter; never actually used.

Dependency injection

Handing an object its collaborators instead of letting it create them.

Seam

A place where behaviour can change without editing the code around it.

MRO

Method resolution order: the list of classes Python searches for an attribute.

Bound method

A method tied to one object, so self is supplied automatically.

Wrapper / decorator

An object that holds another with the same interface and adds behaviour.

Contract test

One checklist run against every implementation of an interface.

Fixture

Reusable test setup, e.g. a pytest function that builds a backend.

Nominal vs structural typing

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.

PROJECT A · EASY

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.

PROJECT B · MEDIUM

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.

PROJECT C · HARD

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

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.

#ProjectFlavor
01🔷 Shape RendererMethod overriding
02🖼️ Image Filter PluginDuck typing
03💱 Currency ConverterOperator overloading
04🍳 Recipe FormatterSingle dispatch
05🎧 Media PlayerMethod overriding
06🧪 Test-Double LabDuck typing ← you are here
07📒 Double-Entry LedgerOperator overloading
08📨 Log RouterSingle dispatch
09♞ Chess PiecesMethod overriding
10✅ Protocol CheckerDuck typing
Read the Full Guide All 10 Projects