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 waystranddictare 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 = hostnamemeans “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.
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.
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:
__init__storinghostnameandipportparameter defaulting to 22summary()— the formatted one-lineris_mgmt_network()— boolean from an attribute testadd_vlan()+ per-instanceself.vlans(the grader checks two instances DON’T share a list — the trap from this lesson, weaponized)
Calling sw1.summary() raises TypeError: summary() takes 0 positional arguments but 1 was given. The likely cause?
Where should self.vlans = [] live so every device gets its own list?
sw1 = NetworkDevice("den-acc-sw01", "10.20.30.11") then sw1.ip = "10.20.30.99". What happened?
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.