1.4.2 · D3Python & Scientific Computing

Worked examples — Functions, classes, and modules

3,164 words14 min readBack to topic

You have already met the parent note: a function is a named block of reusable logic, a class is a blueprint bundling data + behaviour, and a module is a file that groups them. This page does one thing: it drags every one of those ideas through every awkward corner case so that nothing you meet in real code surprises you.

We build from zero. Before we use any new word, we draw it.


The scenario matrix

Think of a function or class as a little machine. The edge conditions are the moments where the machine behaves in a way a beginner does not expect. Here is the full list of cells this topic can throw at you.

Cell Scenario What is tricky here
A Normal call inputs in, value out — the happy path
B Default argument, immutable type a value used only when caller stays silent
C Default argument, mutable trap a default list/dict that secretly persists
D return vs no return the "zero output" degenerate case → None
E Scope: local shadows global same name, two different boxes
F *args and **kwargs unknown number of inputs (the "limiting value" case)
G Class: two instances, separate state each object owns its own data
H Class attribute shared across instances the mutable-default trap, class edition
I self forgotten / method chaining the exam twist
J Module import forms & __name__ one file, two roles (run vs import)

We now cover all ten cells with worked examples. Each is labelled with the cell it hits. Cells C and H are the two whiteboard-vs-card traps promised above.

Figure — Functions, classes, and modules

Example 1 — Cell A: the happy path

Forecast: guess the number before reading. How many of the 4 match?

  1. Define the machine.

    def accuracy(y_true, y_pred):
        correct = sum(t == p for t, p in zip(y_true, y_pred))
        return correct / len(y_true)

    Why this step? zip pairs up matching positions; t == p is True/False, and in Python True counts as 1 when summed. So sum(...) = number of correct predictions.

  2. Call it. accuracy([1,0,1,1], [1,0,0,1]). Why this step? This is cell A — inputs in, one value out, box created then discarded.

  3. Trace the box. Position-by-position: 1==1 ✓, 0==0 ✓, 1==0 ✗, 1==1 ✓ → 3 correct out of 4.

Verify: . Units: a fraction, dimensionless, between 0 and 1 ✓.


Example 2 — Cell B: default argument, immutable type

Forecast: MSE first, then guess RMSE.

  1. Write it.

    def compute_mse(y_true, y_pred, squared=True):
        errors = [(t - p)**2 for t, p in zip(y_true, y_pred)]
        mse = sum(errors) / len(y_true)
        return mse if squared else mse ** 0.5

    Why this step? squared=True is a default: a value the box uses only when the caller stays silent. True is a boolean — immutable (a printed card from our definition above) — so it is completely safe as a default (unlike cell C below).

  2. Silent call: compute_mse([3,5],[4,5]). Why this step? Caller says nothing about squared, so the box fills it with True. Errors: , . Sum , divide by 2 → 0.5.

  3. Loud call: compute_mse([3,5],[4,5], squared=False). Why this step? Now the default is overridden; we take .

Verify: MSE ; RMSE . Check RMSE² gives MSE back ✓.


Example 3 — Cell C: the mutable default trap

Forecast: most people say ["a"] then ["b"]. Write your guess down.

  1. Where does the default live? The list [] is created once, when Python reads the def line — not on every call. Why this step? This is the whole trap. A default value is stored on the function object itself, so a mutable default (list, dict, set — a whiteboard from our definition) is shared across all calls.

  2. First call add_item("a"): the shared list becomes ["a"], returned.

  3. Second call add_item("b"): the same shared list is still ["a"], we append → ["a", "b"]. Why this step? The box did not get a fresh empty list — it reused the survivor from call 1. See the red list in the figure: it never dies.

Figure — Functions, classes, and modules
  1. The fix:
    def add_item(x, bucket=None):
        if bucket is None:
            bucket = []
        bucket.append(x)
        return bucket
    Why this step? None is immutable and safe; we build a fresh list inside every call.

Verify: buggy version → second call returns ["a","b"] (length 2). Fixed version → ["b"] (length 1).


Example 4 — Cell D: return vs no return (the degenerate/zero-output case)

Forecast: guess before reading — is it "hi Ada", or something else?

  1. Separate printing from returning. print shows text on screen; it does not hand a value back to the caller. Why this step? Beginners fuse the two. They are different actions: one is a side-effect, one is the machine's output slot.

  2. The empty output slot. A function with no return (or a bare return) hands back the special object None. Why this step? This is the degenerate case of the matrix: the machine still runs, but its output slot is empty → filled with None.

Verify: result is None is True. Also greet("Ada") prints hi Ada as a side-effect but that string is never captured.


Example 5 — Cell E: scope, local shadows global

Forecast: does the outer x change to 99?

  1. Draw two boxes. There is a global box holding x = 10, and when bump() runs a local box appears holding its own x = 99. Why this step? Assignment inside a function creates a new local name by default — it never reaches out to overwrite the global. See the figure: the local box is walled off.
Figure — Functions, classes, and modules
  1. bump() returns 99 (from the local box), then the local box vanishes.

  2. The outer print reads x — but the local box is gone, so we read the untouched global 10.

Verify: prints 99 10. The global stayed 10 ✓ (assignment inside made a fresh local).


Example 6 — Cell F: *args AND **kwargs (unknown number of inputs — the limiting case)

Forecast: what does describe() with zero arguments give for the sum? And what keys land in opts?

  1. What *nums means. The star collects all positional arguments into one tuple called nums.

    def describe(*nums, **opts):
        s = sum(nums)
        return s, opts

    Why this step? This handles the limiting value of the matrix on the positional side: from 0 arguments up to unbounded, one signature covers them all.

  2. What **opts means. The double-star collects all keyword arguments (name=value pairs the caller writes) into a dictionary called opts. Why this step? Positional (*args) and keyword (**kwargs) are two separate unknown-count channels. nums captures bare values; opts captures named ones. This is the half of cell F that people forget.

  3. describe(1,2,3,4, label="scores", scale=10): nums = (1,2,3,4) so the sum is 10; opts = {"label": "scores", "scale": 10}.

  4. Degenerate describe(): nums = ()sum(()) is defined as 0; opts = {} (empty dict). Why this step? Always test the zero-input corner — here both channels are empty and well-behaved, no crash.

Verify: describe(1,2,3,4, label="scores", scale=10) returns (10, {"label": "scores", "scale": 10}); describe() returns (0, {}).


Example 7 — Cell G: two instances keep separate state

Forecast: guess both numbers.

  1. Each Counter() builds a fresh object with its own self.n = 0 stored inside that object. Why this step? self is the specific object the method was called on. a.tick() edits a's box; it cannot touch b. This is why classes beat loose variables — state is bundled per instance.

  2. Count the ticks. a ticked twice → a.n = 2. b ticked once → b.n = 1.

Figure — Functions, classes, and modules

Verify: a.n == 2, b.n == 1 ✓ (independent state).


Example 8 — Cell H: class attribute shared across instances (the trap, class edition)

Forecast: does p only have "apple"?

  1. Class attribute vs instance attribute. items = [] sits at the class level (not inside __init__), so all instances share the one list — the same mutable trap as cell C. Why this step? self.items.append(...) mutates that single shared whiteboard; it does not create a per-instance list.

  2. Both appends hit the same list["apple", "pear"] for both p.items and q.items.

  3. The fix: put it inside __init__:

    class Basket:
        def __init__(self):
            self.items = []   # now each object gets its own

    Why this step? __init__ runs once per object, so each gets a fresh list.

Verify: buggy p.items == ["apple","pear"] (length 2). Fixed version → p.items == ["apple"] (length 1).


Example 9 — Cell I: the exam twist — self, method chaining

Forecast: guess the three output numbers.

  1. Scaler() makes an object; .fit([2,4,6]) stores self.mean = (2+4+6)/3 = 4 and returns the same object (return self). Why this step? Returning self is what makes .fit(...).transform(...) chain — the exam trick. Without it, fit returns None and .transform would crash with AttributeError.

  2. .transform([2,4,6]) subtracts the mean 4 from each → [-2, 0, 2].

Verify: out == [-2, 0, 2]; also sum(out) == 0 (centered data has zero mean) ✓.


Example 10 — Cell J: module import forms and __name__

Forecast: does the print run when a different file imports this one?

  1. __name__ is a role badge Python stamps on every file. When you run the file directly with python metrics.py, Python sets its __name__ to the special string "__main__". Why this step? The if __name__ == "__main__": line is therefore True only when the file is run as the main program, so the self-test prints self-test: 0.5. This is the "run" role.

  2. When another file imports it, Python sets __name__ to the module's own name — the string "metrics", not "__main__". So the if is False and the self-test block is skipped. Why this step? This is the "import" role — the second life of the same file. The guard is exactly what stops your library's test/demo code from firing every time someone imports it. This is the degenerate/limiting case for modules: one file, behaviour depends on how it is loaded.

  3. The two import forms, side by side. In a different file:

    import metrics                    # form 1: grab the whole module
    metrics.accuracy([1,1], [1,0])    # -> use with the module prefix
     
    from metrics import accuracy      # form 2: grab just this name
    accuracy([1,1], [1,0])            # -> use the bare name, no prefix

    Why this step? import metrics binds the module object (you reach inside with the dot: metrics.accuracy), keeping the namespace tidy. from metrics import accuracy copies just that one name into your file so you can call it directly. Both trigger the import (setting __name__ == "metrics"), so neither runs the self-test.

Verify: the self-test value accuracy([1,1],[1,0]) == 0.5 (one of two positions correct). Importing prints nothing; running directly prints self-test: 0.5.


Recall — lock it in

Recall What makes a list a "mutable" default and a boolean a safe "immutable" one?

A list can be edited in place (whiteboard) so a default list is shared and persists; a boolean/None cannot change (printed card) so it is safe.

Recall A mutable default like

def f(x, acc=[]) is created how often? Once, when the def line is read — so it persists (is shared) across all calls. Use None.

Recall A function with no

return hands back what? ::: None.

Recall Which keyword lets you reassign a global variable from inside a function?

::: global (and nonlocal for an enclosing function's variable).

Recall Why does

fit return self? ::: To enable method chaining like Scaler().fit(x).transform(x).

Recall

describe() with *args/**kwargs and no arguments returns what for its sum and its options dict? ::: sum 0 (because sum(()) is 0) and an empty dict {}.

Recall Inside a function,

x = 99 when a global x = 10 exists does what to the global? ::: Nothing — it creates a fresh local x; the global stays 10.

Recall When a file is imported (not run directly), what is its

__name__, and does its if __name__ == "__main__" block run? ::: __name__ becomes the module's name (e.g. "metrics"), so the block does not run.


Where to go next

  • Turn these classes into real layers → Building neural networks from scratch
  • The .fit().transform() chain is the whole scikit-learn API patterns contract.
  • Vectorize the loops above with NumPy arrays and vectorization.
  • Package these into clean files → Software engineering for ML and Data pipelines and preprocessing.
  • Foundations you may want first: Python basics - syntax, data types, control flow and Object-oriented programming in ML.