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

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)

🧭 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

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

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.

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 port
from 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

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

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.