Checkpoint: OOP & Standard Library
Reviewed & published by Brayan K
A checkpoint is a consolidation stop: you recap the cluster of skills you just learned — abstract base classes, properties, __slots__, scope, custom exceptions, zoneinfo, pickle, and configparser — and prove they fit together by building one small program that uses several at once.
Part of the free Python course at LearnCodingFast — hands-on lessons with examples you run in your browser, plus practice exercises and a quick quiz.
Isolated facts fade; connected skills stick. This stop turns eight separate lessons into one working mental model you can actually reach for in real code.
What This Checkpoint Covers
- Recapping abc, properties, and __slots__
- Recapping scope rules and custom exceptions
- Recapping zoneinfo, pickle, and configparser
- A multi-step build challenge combining them
- A full reference solution to check yourself
- A checkpoint quiz to surface any gaps
1 Recap: The OOP Toolkit
The first cluster sharpened how you design classes — enforcing interfaces, managing attributes, and optimizing memory:
from abc import ABC, abstractmethod
class Sensor(ABC): # ABC: enforce an interface
__slots__ = ("_reading",) # __slots__: drop the per-instance dict
@abstractmethod
def read(self): ... # subclasses MUST implement this
@property # property: computed/validated attribute
def reading(self):
return self._reading
class TempSensor(Sensor):
__slots__ = ()
def __init__(self, value):
self._reading = value
def read(self):
return f"{self._reading} C"
s = TempSensor(21)
print(s.read()) # 21 C
print(hasattr(s, "__dict__")) # False (slotted)- abc — ABC + @abstractmethod enforce a contract; instantiating an incomplete subclass raises TypeError.
- properties — @property exposes computed or validated values behind clean obj.x syntax; setters validate, cached_property memoizes.
- __slots__ — declares fixed attributes, drops __dict__, and shrinks instances at scale (no new, undeclared attributes allowed).
🧭 Recap: Scope & Custom Exceptions
The second cluster covered how names resolve and how to design robust error handling:
# Scope: LEGB lookup, with nonlocal for stateful closures
def make_counter():
n = 0
def step():
nonlocal n # update the ENCLOSING n
n += 1
return n
return step
# Custom exceptions: a hierarchy + chaining with 'from'
class AppError(Exception): ...
class ConfigError(AppError): ...
def load(raw):
try:
return int(raw)
except ValueError as e:
raise ConfigError(f"bad value: {raw!r}") from e # chain it
c = make_counter()
print(c(), c(), c()) # 1 2 3
try:
load("x")
except AppError as e:
print(type(e).__name__, "<-", type(e.__cause__).__name__)
# ConfigError <- ValueError- scope — Python searches Local → Enclosing → Global → Built-in; global/nonlocal let you rebind outer names; assigning a name makes it local everywhere (the UnboundLocalError trap).
- exceptions — subclass Exception into a hierarchy, chain with raise ... from ... (__cause__), use else/finally, and catch only what you can handle.
2 Recap: The Standard-Library Trio
The third cluster added three practical modules for time, persistence, and configuration:
from datetime import datetime, timezone
from zoneinfo import ZoneInfo
import pickle, configparser
from io import BytesIO, StringIO
# zoneinfo: store UTC, display local
utc = datetime(2024, 6, 1, 12, 0, tzinfo=timezone.utc)
print(utc.astimezone(ZoneInfo("Asia/Tokyo")).isoformat()) # ...T21:00:00+09:00
# pickle: serialize Python objects (trusted data only!)
blob = pickle.dumps({"a": 1, "b": [2, 3]})
print(pickle.loads(blob)) # {'a': 1, 'b': [2, 3]}
# configparser: read INI, with typed getters
cfg = configparser.ConfigParser()
cfg.read_string("[app]\nworkers = 4\n")
print(cfg.getint("app", "workers")) # 4- zoneinfo — aware vs naive datetimes; attach IANA zones, convert with astimezone, store in UTC, DST handled for you.
- pickle — dumps/loads and dump/load serialize most objects; never unpickle untrusted data; JSON for anything shared.
- configparser — INI sections + key = value; typed getters (getint/getboolean), [DEFAULT], and fallback.
3 🏗️ Build Challenge: A Validated Service Registry
Combine four ideas into one small system: an abstract base class Service with an abstract protocol(), a port property whose setter raises a custom exception ConfigError for invalid ports, and a concrete url() built from the protocol, name, and port.
- Define ConfigError(Exception).
- Define abstract Service with an abstract protocol() and a validating port property (valid range 1–65535).
- A concrete url() method returning protocol://name:port.
- Subclass it as WebService (protocol "https") and confirm a bad port is rejected.
Start from this runnable scaffold and extend it. Run it after each change:
from abc import ABC, abstractmethod
class ConfigError(Exception):
"""Raised when a service config is invalid."""
class Service(ABC):
def __init__(self, name, port):
self.name = name
self.port = port # routed through the property setter
@property
def port(self):
return self._port
@port.setter
def port(self, value):
if not (1 <= value <= 65535):
raise ConfigError(f"{value} is not a valid port")
self._port = value
@abstractmethod
def protocol(self):
...
def url(self):
return f"{self.protocol()}://{self.name}:{self.port}"
class WebService(Service):
def protocol(self):
return "https"
web = WebService("api", 443)
print(web.url())
# A bad port must raise ConfigError:
try:
WebService("bad", 99999)
except ConfigError as e:
print("Rejected:", e)
# TODO: add a DbService (protocol "postgres") on port 5432 and print its url()
# Expected output (before your TODO):
# https://api:443
# Rejected: 99999 is not a valid portfrom abc import ABC, abstractmethod
from datetime import datetime, timezone
from zoneinfo import ZoneInfo
class ConfigError(Exception):
"""Raised when a service config is invalid."""
class Service(ABC):
__slots__ = ("name", "_port") # memory-lean instances
def __init__(self, name, port):
self.name = name
self.port = port # validated via the setter
@property
def port(self):
return self._port
@port.setter
def port(self, value):
if not (1 <= value <= 65535):
raise ConfigError(f"{value} is not a valid port")
self._port = value
@abstractmethod
def protocol(self):
...
def url(self):
return f"{self.protocol()}://{self.name}:{self._port}"
class WebService(Service):
__slots__ = ()
def protocol(self):
return "https"
class DbService(Service):
__slots__ = ()
def protocol(self):
return "postgres"
# Build a small registry
registry = [WebService("api", 443), DbService("primary", 5432)]
for svc in registry:
print(svc.url())
# Validation: invalid port -> custom exception
try:
WebService("bad", 99999)
except ConfigError as e:
print("Rejected:", e)
# ABC enforcement: cannot instantiate the abstract base
try:
Service("x", 80)
except TypeError as e:
print("Cannot instantiate Service:", type(e).__name__)
# __slots__: no per-instance dict
print("Has __dict__?", hasattr(registry[0], "__dict__"))
# zoneinfo: a UTC deploy time shown in Tokyo
deploy = datetime(2024, 5, 1, 12, 0, tzinfo=timezone.utc)
print("Deploy (Tokyo):", deploy.astimezone(ZoneInfo("Asia/Tokyo")).isoformat())
# Output:
# https://api:443
# postgres://primary:5432
# Rejected: 99999 is not a valid port
# Cannot instantiate Service: TypeError
# Has __dict__? False
# Deploy (Tokyo): 2024-05-01T21:00:00+09:00📝 Checkpoint Quiz
1. What happens when you try to instantiate a subclass of an ABC that hasn't implemented every @abstractmethod?
Python raises TypeError at instantiation. The exact wording changed in 3.12 — "Can't instantiate abstract class Wallet without an implementation for abstract method 'refund'" on 3.12 and later, "... with abstract method refund" before that — so match on the exception type rather than the message. The class stays abstract until all required methods are implemented; Wallet.__abstractmethods__ lists the ones still missing.
2. You add __slots__ = ("x", "y") to a class. What can you no longer do, and why?
You can no longer assign attributes that aren't listed in __slots__ — doing so raises AttributeError. There's no per-instance __dict__ to hold arbitrary new attributes, which is exactly what makes the instances smaller.
3. Inside a closure, you want an inner function to update a variable defined in the enclosing function. Which keyword do you use — global or nonlocal?
nonlocal. It rebinds the nearest enclosing function's variable. global would reach all the way out to the module level instead, missing the enclosing scope entirely.
4. What's the difference between raise NewError(...) and raise NewError(...) from original?
from original sets __cause__, explicitly chaining the new exception to the one that caused it; the traceback shows "direct cause." Without from, the original is only kept implicitly as context and the link is less clear.
5. Why should times be stored in UTC and converted to a local zone only for display?
UTC has no daylight saving and never shifts, so it's an unambiguous baseline for storage and comparison. Converting to a local zone with astimezone only at display time avoids DST-related bugs.
6. A teammate wants to pickle.loads() a blob downloaded from a public URL. What's wrong, and what should they use instead?
Unpickling untrusted data can execute arbitrary code — a serious security hole. For data from an external/untrusted source they should use json (text, safe, language-neutral). Reserve pickle for trusted, Python-to-Python state.
Quick Pitfall Check
❌ Routing __init__ around the validating setter
self._port = port # writes straight to storage, skips validation!✅ Assign through the property name so the setter runs: self.port = port.
❌ Forgetting __slots__ = () on a slotted subclass
class WebService(Service): # no __slots__ -> __dict__ comes back!
def protocol(self): ...✅ Add an empty __slots__ = () to each subclass to keep the memory savings.
📚 Keep Learning
- Classes (tutorial) — a refresher on the object model these OOP features build on.
- The Python Standard Library — the index where abc, datetime, zoneinfo, pickle, and configparser all live.
- Errors and Exceptions — revisit hierarchies, chaining, and clean-up actions if the quiz tripped you up.
Frequently Asked Questions
Checkpoint cleared — your OOP & stdlib skills connect now!
You recapped abstract base classes, properties, __slots__, scope, custom exceptions, zoneinfo, pickle, and configparser — and combined four of them into a validated service registry. These aren't isolated tricks anymore; they're a toolkit you can compose.
Practice quiz
What happens when you try to instantiate a subclass of an ABC that has not implemented every @abstractmethod?
- It instantiates but the missing method returns None
- It raises NotImplementedError when the method is called
- Python raises TypeError at instantiation
- Nothing — abstract methods are only a hint
Answer: Python raises TypeError at instantiation. Instantiating an abstract class with unimplemented abstract methods raises TypeError immediately. The class stays abstract until all required methods are implemented.
You add __slots__ = ("x", "y") to a class. What can you no longer do?
- Assign attributes not listed in __slots__
- Subclass the class
- Use @property on the class
- Call methods on instances
Answer: Assign attributes not listed in __slots__. With __slots__ there is no per-instance __dict__, so assigning any attribute not declared in __slots__ raises AttributeError. That missing dict is exactly what shrinks the instances.
Inside a closure, which keyword lets an inner function rebind a variable in the enclosing function?
- global
- static
- enclosing
- nonlocal
Answer: nonlocal. nonlocal rebinds the nearest enclosing function scope. global would reach all the way to module level instead, skipping the enclosing scope.
What does writing raise NewError(...) from original do that raise NewError(...) alone does not?
- It suppresses the original traceback
- It sets __cause__, explicitly chaining the exceptions
- It re-raises the original exception
- It converts the error into a warning
Answer: It sets __cause__, explicitly chaining the exceptions. The from clause sets __cause__ and marks the new exception as a direct cause of the original, producing a clear chained traceback.
In the LEGB rule, what does the E stand for?
- Enclosing
- Exception
- External
- Environment
Answer: Enclosing. LEGB is Local, Enclosing, Global, Built-in — the order Python searches for a name. Enclosing covers names in an outer function.
Why should datetimes be stored in UTC and converted to a local zone only for display?
- UTC strings are shorter
- Local zones cannot be stored in a database
- UTC has no daylight saving and never shifts, giving an unambiguous baseline
- zoneinfo only works with UTC
Answer: UTC has no daylight saving and never shifts, giving an unambiguous baseline. UTC never observes DST and never shifts, so it is an unambiguous baseline. Converting with astimezone only at display time avoids DST bugs.
A teammate wants to pickle.loads() a blob downloaded from a public URL. What is the danger?
- Pickle is slower than JSON
- Unpickling untrusted data can execute arbitrary code
- Pickle cannot serialize dictionaries
- The blob would be too large
Answer: Unpickling untrusted data can execute arbitrary code. Unpickling untrusted data can run arbitrary code — a serious security hole. For external data use json instead; reserve pickle for trusted, Python-to-Python state.
Which configparser method reads an integer value and converts it to an int automatically?
- cfg.read_int("app", "workers")
- cfg.get("app", "workers", type=int)
- cfg.parse_int("app", "workers")
- cfg.getint("app", "workers")
Answer: cfg.getint("app", "workers"). ConfigParser exposes typed getters like getint, getboolean, and getfloat that read the raw string and convert it for you.
What does the @property decorator let you do?
- Make a method run in a background thread
- Expose a computed or validated value behind clean obj.x attribute syntax
- Mark a method as abstract
- Cache a function across calls automatically
Answer: Expose a computed or validated value behind clean obj.x attribute syntax. A property exposes a method behind attribute syntax; the setter can validate values, and cached_property memoizes the result.
In the build challenge, why must __init__ assign self.port = port rather than self._port = port?
- _port is a reserved name
- self._port would raise AttributeError
- Assigning through self.port runs the validating property setter
- Both lines behave identically
Answer: Assigning through self.port runs the validating property setter. Assigning through the property name self.port routes the value through the setter so validation runs. Writing self._port directly skips the check.
Continue this course
- Previous: Config Files with configparser
- Next: Threading & the GIL — Run concurrent I/O with threads and understand why the GIL limits CPU work
- Quick reference: Python cheat sheet
Frequently asked questions
What is this checkpoint for?
This checkpoint consolidates the OOP and standard-library module you've just learned — abstract base classes, properties, __slots__, scope, custom exceptions, zoneinfo, pickle, and configparser. You'll recap each idea, then combine several of them in one build challenge so they click together rather than living as isolated facts.
I'm stuck on the build challenge — what should I do?
Start from the runnable starter, get one piece working at a time, and run it after every small change. If you're truly stuck, open the full solution, read it carefully, then close it and try to reproduce it from memory. Struggling first and then checking is how the material sticks.
How do I know if I'm ready to move on?
If you can answer the checkpoint quiz without peeking and complete the build challenge with only occasional reference to the docs, you're ready. If several quiz answers surprise you, revisit that specific lesson before continuing — checkpoints are designed to surface exactly those gaps.