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("acc-sw01", "10.20.30.11")
>>> sw2 = NetworkDevice("acc-sw02", "10.20.30.12")
>>> sw1.summary()
'acc-sw01 (10.20.30.11)'
>>> sw2.hostname
'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 = "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='acc-sw01'<br/>ip='10.20.30.11'<br/>vlans=[10, 20]"]
C -- "NetworkDevice(...)" --> B["sw2<br/>hostname='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.
Prove it (graded lab)
Write each piece in the editor, then Run grader - it runs the lesson’s
real checks right here in your browser, nothing to install. Green means the
skill is yours, and this NetworkDevice class is the one the Lesson 10
capstone (netaudit) leans on.
One class, built up across five graded behaviors - the editor 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)
Reveal a reference solution
"""Lesson 7 reference solutions. Read after an honest attempt."""
class NetworkDevice:
def __init__(self, hostname, ip, port=22):
self.hostname = hostname
self.ip = ip
self.port = port
self.vlans = []
def summary(self):
return f"{self.hostname} ({self.ip}:{self.port})"
def is_mgmt_network(self):
return self.ip.startswith("10.20.30.")
def add_vlan(self, vlan_id):
if vlan_id not in self.vlans:
self.vlans.append(vlan_id)
Read it, close it, then re-type your own version from memory. Wrong turns are where the learning is.
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("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.