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.

TL;DR - only got 10 minutes? This is the lesson. Hit to have it read to you.
  • A class bundles data (attributes) with the functions that operate on it (methods). class NetworkDevice: + def __init__(self, hostname, ip): is the pattern.
  • __init__ runs once per new object; self is the object being built. self.hostname = hostname stores the data ON the instance.
  • sw1 = NetworkDevice("acc-sw01", "10.20.30.11") creates an instance. Each instance is independent - sw2's attributes never touch sw1's.
  • Methods are functions defined inside the class; they take self first and reach the object's own data through it: self.ip, self.hostname.
  • The error "takes 1 positional argument but 2 were given" almost always means you forgot self in a method definition.
  • Per-instance lists/dicts get created INSIDE __init__ (self.vlans = []) - never as class-level or default-argument values, or instances share one container.
  • Nothing new under the sun: "den".upper() was always a method on a str object. You've been using classes since Lesson 1 - now you can build them.
  • Prove it: the graded lab at the end runs in your browser - implement the behaviors and make every check green.
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("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.

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='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.

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.

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:

  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)
Graded lab - 0 checks, run right here
Edit the functions above, then Run grader. First run downloads the Python runtime (~10 MB), so give it a few seconds.
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.

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.