Classes and Objects, Part 1

Your device data lives in dicts; the functions that operate on it live somewhere else, and you keep carrying both around together. A class moves them in together — and it's also the moment Python itself stops being mysterious, because everything you've used all course was an object.

In this lesson you will:
  • Define a class with __init__ and understand what self actually is
  • Create independent instances and read/write their attributes
  • Write methods — functions that live with their data
  • Recognize that strings, lists, and dicts were classes all along
  • Initialize per-instance containers safely inside __init__

The problem: data here, behavior there

By Lesson 6 your code has settled into a pattern: a dict holds a device’s facts, and a family of functions — summary(facts), is_reachable(facts), normalize(facts) — takes that dict as its first argument. The data and its behavior are a couple that you keep introducing to each other.

A class makes the relationship official:

class NetworkDevice:
    def __init__(self, hostname, ip):
        self.hostname = hostname
        self.ip = ip

    def summary(self):
        return f"{self.hostname} ({self.ip})"

Read it in three pieces:

  • class NetworkDevice: — declares a new type, the same way str and dict are types. Capitalized names are the convention.
  • __init__ — the initializer. It runs automatically every time you create a new device, and its job is to store the data.
  • self — the object under construction (and later, the object a method was called on). self.hostname = hostname means “store this on this particular device.”

Instances: each one its own

>>> sw1 = NetworkDevice("den-acc-sw01", "10.20.30.11")
>>> sw2 = NetworkDevice("den-acc-sw02", "10.20.30.12")
>>> sw1.summary()
'den-acc-sw01 (10.20.30.11)'
>>> sw2.hostname
'den-acc-sw02'

Calling the class like a function builds an instance. sw1 and sw2 are separate objects with separate attributes — renaming one switch (sw2.hostname = "den-acc-sw02-new") changes nothing about the other, exactly as it should be. Attributes read and write with plain dot syntax; no getters or setters required — this is Python, not Java.

Notice what happened to the function-vs-data split: summary() takes no arguments at the call site, because it already knows which device it belongs to. That’s what self buys you.

⬡ One blueprint, independent instances
Rendering diagram…
View diagram source — it's just text (Mermaid). Diagrams-as-code is how modern network docs work; the flagship course has a free module on it.
flowchart TD
  C["class NetworkDevice<br/>__init__ + summary()"] -- "NetworkDevice(...)" --> A["sw1<br/>hostname='den-acc-sw01'<br/>ip='10.20.30.11'<br/>vlans=[10, 20]"]
  C -- "NetworkDevice(...)" --> B["sw2<br/>hostname='den-acc-sw02'<br/>ip='10.20.30.12'<br/>vlans=[]"]

Per-instance containers — init is the safe place

Devices accumulate things: VLANs, tags, findings. Per-instance containers get created inside __init__:

class NetworkDevice:
    def __init__(self, hostname, ip, port=22):
        self.hostname = hostname
        self.ip = ip
        self.port = port          # Lesson 6 defaults work here too
        self.vlans = []           # fresh list for EVERY instance

    def add_vlan(self, vlan_id):
        if vlan_id not in self.vlans:
            self.vlans.append(vlan_id)

self.vlans = [] inside __init__ runs once per instance — every device gets its own list. This is the class-shaped echo of Lesson 6’s mutable-default trap: a list created anywhere shared (a default argument, the class body itself) becomes one list across all devices, and VLANs from switch one haunt switch two.

You’ve been here all along

>>> "den".upper()
'DEN'
>>> [10, 20].append(30)

str and list are classes. .upper() and .append() are methods. dir("den") from Lesson 1 was listing a class’s methods. The whole language has been objects since your first REPL session — the only new thing today is that you’re now on the authoring side.

🖥 Data and behavior, finally together
▶ Try it yourself (Python runs in your browser)
Output appears here. First run downloads the Python runtime (~10 MB), so give it a few seconds.

Exercises (graded)

cd labs/python-foundations/lesson07
pytest -q

One class, built up across five graded behaviors — exercises.py gives you the skeleton:

  1. __init__ storing hostname and ip
  2. port parameter defaulting to 22
  3. summary() — the formatted one-liner
  4. is_mgmt_network() — boolean from an attribute test
  5. add_vlan() + per-instance self.vlans (the grader checks two instances DON’T share a list — the trap from this lesson, weaponized)
✅ Check your understanding

Calling sw1.summary() raises TypeError: summary() takes 0 positional arguments but 1 was given. The likely cause?

1 / 3

Summary

A class is the marriage of data and behavior: __init__ stores attributes on self, methods reach back through self to use them, and every instance is an independent object — including its containers, provided they’re born inside __init__. The off-by-one TypeError means a missing self, and the deepest takeaway is retroactive: strings, lists, and dicts were classes all along, so the entire language just became one consistent idea. Next lesson finishes the arc — inheritance for multivendor device trees, and the dunder methods that make your objects print, compare, and debug like the built-ins do.