We can't find the internet
Attempting to reconnect
Something went wrong!
Attempting to reconnect
← Precise Type Hints with Literals and Generics step 21 of 26
Find Out What isinstance Checks on a Protocol
You have a list of objects from several libraries, and you want to close every
one that has a close method. They share no base class. A Protocol describes
them anyway. A Protocol is a class that lists the methods an object must
have. Any object with those methods matches it, without inheriting from it.
The next track covers Protocols in depth.
from typing import Protocol, runtime_checkable
@runtime_checkable
class SupportsClose(Protocol):
def close(self) -> None: ...
@runtime_checkable lets you write isinstance(obj, SupportsClose). Without
it, that call raises TypeError. After the check, mypy treats obj as having
close(self) -> None.
The runtime check looks only for an attribute named close. It does not
check that close is a method, or what arguments it takes:
class NotCallable:
close = 3
isinstance(NotCallable(), SupportsClose) # True
NotCallable().close() # TypeError: 'int' object is not callable
So even after the check passes, the call can still fail. This exercise counts how often that happens.
What to write
Add @runtime_checkable to SupportsClose. Keep the fixture classes and
FACTORIES as they are.
close_all(objects: Iterable[object]) -> int:
- Read the objects once, in order.
- For each object that passes
isinstance(obj, SupportsClose), callobj.close()with no arguments. - Count a success each time
close()returns normally, whatever it returns. - Put only the
obj.close()call insidetry, and catchException. On an exception, skip that object and go on. - Do not catch
BaseException, soKeyboardInterruptstill stops the loop. - Close an object each time it appears. An empty input gives
0.
The fixtures
| Kind | Class | Passes the check | What close() does |
|---|---|---|---|
plain |
PlainCloser |
yes | sets closed to True |
raising |
RaisingCloser |
yes | raises OSError |
arity |
ArityMismatch |
yes | raises TypeError for the missing force argument |
notcallable |
NotCallable |
yes | raises TypeError, because close is 3 |
dynamic |
DynamicCloser |
no | not called |
noclose |
NoClose |
no | not called |
DynamicCloser returns close from __getattr__, which Python calls only
for attributes it cannot find. The check does not call __getattr__, so it
does not find close.
What solve returns
solve(kinds: list[str]) -> tuple[int, list[str]]:
- Create one object per kind with
FACTORIES[kind](), in order and with duplicates. An unknown kind raisesKeyError. Do not skip it. - Before closing anything, list the class names of the objects that pass the check, in order and with duplicates.
- Return
(close_all(objects), names).
assert solve(["plain", "plain"]) == (2, ["PlainCloser", "PlainCloser"])
assert solve(["plain", "raising", "arity", "notcallable", "dynamic", "noclose"]) == (
1, ["PlainCloser", "RaisingCloser", "ArityMismatch", "NotCallable"],
)
assert solve([]) == (0, [])
Going further
Since Python 3.12, the check uses inspect.getattr_static. That is why
__getattr__ cannot satisfy it, and why a property named close passes
without Python running it. A hasattr check does not look at signatures
either.
See the runtime_checkable documentation.
Your code, saved here
Sign in to access your accepted solutions and saved draft history. You can keep reading the lesson without an account.
Sign in to your accountStuck?
Python reference solution
Sign in to track your attempts toward unlocking the reference solution.