Mental model
A 20-line script fits in your head. A 500-line script does not. The difference between the two is how you organise the code: into functions, classes and modules.
Cisco objective 1.5 (CCNAAUTO 200-901) asks you to explain the benefits of organising code into these three building blocks. This topic is the explanation.
The rule of thumb for each:
- A function replaces repetition. The moment you copy-paste the same lines twice, extract a function.
- A class replaces “pass this same bundle of 5 arguments to every function”. Group the state and the behavior together.
- A module replaces one giant file. Split by responsibility (one file for parsing, one for API calls, one for the main loop).
Functions: reusable blocks
Before (repetitive)
from netmiko import ConnectHandler
dev1 = {"device_type": "cisco_ios", "host": "10.0.0.1", "username": "admin", "password": "x"}
with ConnectHandler(**dev1) as c:
print(c.send_command("show version | i Cisco IOS"))
dev2 = {"device_type": "cisco_ios", "host": "10.0.0.2", "username": "admin", "password": "x"}
with ConnectHandler(**dev2) as c:
print(c.send_command("show version | i Cisco IOS"))
dev3 = {"device_type": "cisco_ios", "host": "10.0.0.3", "username": "admin", "password": "x"}
with ConnectHandler(**dev3) as c:
print(c.send_command("show version | i Cisco IOS"))
After (one function)
from netmiko import ConnectHandler
def show_version(host, username="admin", password="x"):
dev = {"device_type": "cisco_ios", "host": host, "username": username, "password": password}
with ConnectHandler(**dev) as c:
return c.send_command("show version | i Cisco IOS")
for h in ["10.0.0.1", "10.0.0.2", "10.0.0.3"]:
print(show_version(h))
Benefits:
- Readable. The main loop reads like English: for each host, show its version.
- Changeable in one place. Switch to
show version | i Softwareonce, every caller gets it. - Testable. You can call
show_version("10.0.0.1")from a unit test with a mocked ConnectHandler. - Default arguments keep the common case short while allowing overrides.
Rules of thumb for functions
- A function should do one thing. If you cannot name it in a sentence, split it.
- 20 lines of body is a soft limit. Longer usually means two things in one function.
- Return a value instead of printing. The caller decides what to do with it (print, write to a file, send to an API).
Classes: grouped state and behavior
Use a class when the same bundle of data (host + user + password + platform) is passed into many functions, OR when there is state that persists between calls (an open SSH connection, a running counter).
Before (loose functions, repeated bundle)
def show_version(host, user, password, platform): ...
def show_run(host, user, password, platform): ...
def push_config(host, user, password, platform, lines): ...
# Every call repeats the bundle:
show_version("10.0.0.1", "admin", "x", "cisco_ios")
show_run("10.0.0.1", "admin", "x", "cisco_ios")
push_config("10.0.0.1", "admin", "x", "cisco_ios", ["interface gi0/0", "description WAN"])
After (one class)
from netmiko import ConnectHandler
class Device:
def __init__(self, host, user="admin", password="x", platform="cisco_ios"):
self.host = host
self.user = user
self.password = password
self.platform = platform
def _conn(self):
return ConnectHandler(
device_type=self.platform, host=self.host,
username=self.user, password=self.password,
)
def show_version(self):
with self._conn() as c:
return c.send_command("show version | i Cisco IOS")
def push_config(self, lines):
with self._conn() as c:
return c.send_config_set(lines)
# Usage:
d = Device("10.0.0.1")
print(d.show_version())
d.push_config(["interface gi0/0", "description WAN"])
Benefits:
- One place to construct the bundle (
Device("10.0.0.1")). - Methods know their state without the caller passing it every time.
- Easy to subclass when a new platform needs small overrides (e.g.
class CatalystCenter(Device)with its own_conn).
When NOT to use a class
- The function has no state and never shares its arguments with other functions. Keep it a function.
- Only one instance will ever exist. A module with top-level functions is simpler.
Modules: grouped files
A module is just a .py file you can import from another .py. Split a script into modules when:
- The file is more than ~300 lines.
- You have clearly separable responsibilities (one for parsing, one for API, one for CLI).
- Multiple scripts need the same functions.
Example layout
net-tools/
├── __init__.py # (makes net-tools a Python package; can be empty)
├── device.py # the Device class above
├── parsers.py # show-command output parsers
├── api_meraki.py # Meraki Dashboard API wrapper
└── cli.py # the main() that parses argv and dispatches
From cli.py you can:
from net_tools.device import Device
from net_tools.parsers import parse_show_interface
Benefits:
- Find anything fast. You know where parsers live, where device classes live.
- Import only what you need. If a script does not touch Meraki, it does not pay the cost of loading that module.
- Reusable. A second script just imports what it needs; no copy-paste.
- Easier to test. You can test
parsers.pyin isolation without needing a device connection.
When to apply each
| You find yourself… | Reach for… |
|---|---|
| copying 5 lines into a second place | a function |
| passing the same 4 arguments into every function | a class |
| scrolling for 10 seconds to find a function | split into modules |
| writing tests for a 300-line file | split into modules; tests get easier |
The #1 mistake
Making a class because the tutorial you read used classes. If you are writing a 50-line script that connects to one device and prints one show-command output, you do not need a class. Just a function or two. Classes have a cost: more boilerplate, more concepts, harder to grok at a glance. Only pay that cost when the state/bundle problem actually appears.
FAQ
What is self? The first parameter of every method inside a class. Python passes the instance in automatically when you call d.show_version(). You never type self at the call site; you type it in the method definition.
What is __init__? The special method Python calls when you create an instance (Device("10.0.0.1")). Use it to store initial state on self.
Do I need to put every function in a class? No. If a function has no related state, keep it a top-level function in a module.
What is __name__ == "__main__" for? A guard so that code only runs when the file is executed directly (python cli.py), not when it is imported from another file. The pattern:
def main():
...
if __name__ == "__main__":
main()
What is a package? A folder with an __init__.py file in it. Treated by Python as an importable unit. The net-tools/ layout above is a package.
