CCNP Automation 350-901 · Start here · Python 3

Python 3: functions, files & handling errors

Write code once and reuse it for every router with functions, save and read results with files, and keep a script alive when a router is down using try/except. You will also learn to read a traceback from the bottom up.

45 min read9 chapters2 labs15 quiz7 scenarios15 interview Q&A

This first module is free: read the lesson and take the quiz. Create a free account to run up to 3 hands-on labs.

Log inStart free
Jump to chapter (9)
01

Welcome: why functions, files and error handling matter

What you will learn in this module. You will learn three skills that turn a short Python experiment into a script you can trust on a real network. First, functions: write a block of code once and use it for every router. Second, files: save results to disk and read prepared data back. Third, exceptions: read the error message Python prints when something breaks, and make the script carry on when one device is down. At the end you will build a configuration generator and a router checker, exactly like the two labs of this module. This is the Python part of the CCNP Automation (350-901 AUTOCOR) track.

Prerequisites. You do not need to have written code before this track, but the two modules before this one are assumed: Python 1 (print, variables, strings and f-strings) and Python 2 (lists, dictionaries, loops and if). If you can explain what for ip in ROUTERS: does, you are ready. Everything else in this module is explained line by line.

Analogy: a recipe card

A restaurant does not invent the recipe for butter chicken every time an order arrives. The cook has a recipe card. The ingredients change (500 g or 2 kg of chicken), but the steps stay the same. A function is that recipe card. You write the steps once, give the card a name, and every time you need the dish you say the name and hand over the ingredients. The ingredients you hand over are called arguments. The finished dish you get back is the return value.

Inputsname, ip, descFunctionbuild_intf_configResultlist of config lines

A function is a small machine

The problem functions solve

Imagine a company opens a branch and the router needs three LAN interfaces. Each interface needs the same four lines; only the name, the IP address and the description change. Here is the copy-paste way. Read it slowly: print() shows text on the screen, and every line below is one print().

print("interface GigabitEthernet0/1")
print(" description LAN-USERS")
print(" ip address 10.10.1.1 255.255.255.0")
print(" no shutdown")
print("interface GigabitEthernet0/2")
print(" description LAN-CCTV")
print(" ip address 10.10.2.1 255.255.255.0")
print(" no shutdown")

Real output, checked in the lab terminal.

interface GigabitEthernet0/1
 description LAN-USERS
 ip address 10.10.1.1 255.255.255.0
 no shutdown
interface GigabitEthernet0/2
 description LAN-CCTV
 ip address 10.10.2.1 255.255.255.0
 no shutdown

It works, and for two interfaces it is fine. Now think about 50 branches with 3 interfaces each. That is 150 copies of the same four lines. If you find a mistake (for example the mask should have been 255.255.255.252), you must fix it in 150 places and you will surely miss one. This is how outages are born: not from hard problems, but from tired people repeating the same thing.

The same job with a function

Do not worry about every word yet; the next chapters explain each piece. Just notice the shape: the four lines exist once, and the last two lines of the script simply call it twice.

def show_interface(name, ip, desc):
    print("interface " + name)
    print(" description " + desc)
    print(" ip address " + ip + " 255.255.255.0")
    print(" no shutdown")

show_interface("GigabitEthernet0/1", "10.10.1.1", "LAN-USERS")
show_interface("GigabitEthernet0/2", "10.10.2.1", "LAN-CCTV")

Real output, checked in the lab terminal.

interface GigabitEthernet0/1
 description LAN-USERS
 ip address 10.10.1.1 255.255.255.0
 no shutdown
interface GigabitEthernet0/2
 description LAN-CCTV
 ip address 10.10.2.1 255.255.255.0
 no shutdown

The output is identical to the copy-paste version. The difference is that a change to the mask is now one edit in one place.

Why files and error handling come with functions

Functions make the script short. But a real script also needs to remember things and to survive things:

  • A script forgets everything when it ends. A file is how it keeps a backup, a report or a list of failed devices, and how it reads a device list somebody else prepared.
  • Real networks are never perfect: a router is rebooting, a password changed, a cable is out. Without protection, the script crashes on router 2 and never reaches routers 3 to 50. try/except lets the script note the problem and continue.

The lab devices you will meet

In the labs the automation host is called AUTO (IP 10.99.0.100) and the routers are R1, R2 and R3 on a switch, with management addresses 10.99.0.1, 10.99.0.2 and 10.99.0.3 and the SSH login netops / NK@2026. In lab 1 you finish a file called intf.py, build branch.cfg and push it to R1. In lab 2 you repair check.py, which collects the uptime of three routers and writes results.txt.

Functionsdef, params, returnFilesread, write, appendErrorstraceback, try/exceptLabsbuild and repair

Your path through this module

Common beginner mistake. Thinking a function is only for long or clever code. A function is worth it the moment you catch yourself copying a block. Also, beginners often write 200 lines at the top level of a file and then cannot find the bug. Small named functions make bugs easy to find.

Exam trap. The exam may show a function with no return and ask what a variable holds after calling it. The answer is None. Remember that print() shows a value and does not give it back.

Fifty branches, one typo

A retail company configured 50 branch routers by pasting the same interface block into a script 50 times. One copy had 255.255.255.254 instead of 255.255.255.252. Eleven days later that branch lost its WAN link after a maintenance window and the team spent two hours finding the one bad copy. After the incident they rewrote the script around one function and one data list. The next mask change took one edit and one run.

Lesson: one copy of the logic means one place to fix, and one place to review.

"Why would a network engineer use functions in an automation script?"

Functions remove duplication, so a fix is made once instead of in many places. They give the code readable names such as build_intf_config, they can be tested on their own, and the same function can be reused by many scripts. Say that the arguments change from device to device while the logic stays the same.

Key takeaways

  • A function is a named block of code you write once and call many times.
  • Arguments go in, a return value comes out.
  • Copy-pasted config lines are the main source of automation typos.
  • Files let a script keep results; try/except lets it survive a dead device.
  • The two labs use AUTO, R1 to R3, and SSH login netops / NK@2026.
02

Your first function: def and calling it

In this chapter you will write your first function and learn the one idea everything else builds on: defining a function and calling it are two different steps. We use only one small idea per chapter, so do not hurry; run every example in your own lab terminal.

What a function looks like

Here is the smallest useful function. It prints a banner line that you might put at the top of a report.

def banner():
    print("=== Branch router audit ===")

banner()
banner()

Real output, checked in the lab terminal.

=== Branch router audit ===
=== Branch router audit ===

Line by line:

  • def banner(): The word def (short for "define") tells Python: "I am teaching you a new command." banner is the name I chose. The brackets () are where inputs would go; they are empty for now. The colon : at the end says "the body follows".
  • print("=== Branch router audit ===") This line is the body. It is moved to the right by 4 spaces. That indentation is how Python knows which lines belong to the function. There are no curly brackets in Python; the spaces are the structure.
  • banner() This is a call. The name followed by brackets means "run that function now".

Notice that the banner printed twice, because we called it twice. The def lines alone print nothing: defining a function is like writing the recipe card and putting it in the drawer. Nothing is cooked until somebody calls it.

def banner():stored, nothing runsbanner()the body runsbanner()the body runs again

def stores the recipe, the call cooks it

Indentation is part of the language

The body can have many lines. Every line must be indented by the same amount (use 4 spaces; do not mix tabs and spaces). The first line that is not indented ends the function.

def banner():
    print("=== Branch router audit ===")
    print("Run by: automation host AUTO")

print("Before the call")
banner()
print("After the call")

Real output, checked in the lab terminal.

Before the call
=== Branch router audit ===
Run by: automation host AUTO
After the call

The two print lines inside the function run only when banner() is called, so they appear between "Before the call" and "After the call". The last two print lines are not indented, so they are outside the function and run immediately.

Calling a function inside a loop

You already know for loops from the previous module. A function plus a loop is where automation starts to pay off: one function, many devices.

def check_router(name):
    print("Checking", name)

for router in ["R1", "R2", "R3"]:
    check_router(router)

Real output, checked in the lab terminal.

Checking R1
Checking R2
Checking R3

Here name is a placeholder inside the function. Each time the loop calls check_router(router), the current value of router is dropped into name. We cover these placeholders properly in the next chapter; for now just see that the same code ran three times.

Choosing good names

  • Use lowercase words joined by underscores: check_router, build_intf_config. This style is called snake_case.
  • Start with a verb that says what the function does: get_uptime, save_report.
  • A name cannot start with a digit or contain spaces or dashes. check-router is wrong; check_router is right.
  • Do not reuse the names of built-in tools such as print, list or open.

The order matters: define first, call later

Python reads the file from top to bottom. If you call a function before the def line has been read, Python does not know the name yet.

check_router("R1")

def check_router(name):
    print("Checking", name)

Real output, checked in the lab terminal.

Traceback (most recent call last):
  File "nameerr.py", line 1, in <module>
    check_router("R1")
NameError: name 'check_router' is not defined

Read the last line of the traceback first: NameError: name 'check_router' is not defined. Newer Python versions may also print little ^^^^ markers under the code line; they only point at the problem, so you can ignore them. The fix is to move the call below the def. We read tracebacks fully in a later chapter.

Common mistakes. (1) Forgetting the colon after def name(): gives a SyntaxError. (2) Forgetting the brackets in the call: writing banner instead of banner() does not run the function; Python just treats it as a name and does nothing visible. (3) Indenting the body with 3 spaces on one line and 4 on the next gives an IndentationError.

Exam trap. A question may show a def with a print inside and no call, then ask what is printed. The answer is nothing. Always look for the call.

The script that printed nothing

A junior engineer wrote a function backup_all() that looped over twelve routers, ran it, and saw an empty screen. He suspected SSH. A colleague opened the file, saw the def backup_all(): line at the bottom and no call after it, and added backup_all() as the last line. The backup ran at once. The code was never wrong; it was simply never called.

Lesson: when a script does nothing, check that the function is called before you debug the network.

"What is the difference between defining and calling a function?"

Defining with def creates the function and stores it under a name, but the body does not run. Calling it with brackets, such as banner(), runs the body. A function must be defined before the line that calls it executes.

Key takeaways

  • def name(): followed by an indented body defines a function.
  • Defining does not run the body; calling with name() does.
  • The 4-space indentation marks which lines belong to the function.
  • Use snake_case verb names such as check_router.
  • Define before you call, otherwise you get a NameError.
03

Parameters and arguments: the blanks in your template

A function that always does exactly the same thing is not very useful. The real power comes when the function can be given different values each time. This chapter explains how values go in.

The blank in the recipe

Think of a printed config template: interface ______ / ip address ______. The blanks are empty on the printed form, and an engineer fills them in. In Python the blanks have names and live in the brackets after the function name. They are called parameters. The actual values you fill in when you call the function are called arguments.

def show_interface(name):
    print("interface " + name)
    print(" no shutdown")

show_interface("GigabitEthernet0/1")
show_interface("Loopback0")

Real output, checked in the lab terminal.

interface GigabitEthernet0/1
 no shutdown
interface Loopback0
 no shutdown

Here name is the parameter. In the first call the argument is "GigabitEthernet0/1", so inside the function name holds that text. In the second call it holds "Loopback0". The parameter exists only while the function runs.

Callshow_interface("Gi0/1")Argument"Gi0/1" is the real valueParametername receives itBodyuses name

Parameter versus argument

Several parameters, and their order

Separate parameters with commas. When you call the function with plain values, they are matched by position: the first argument goes to the first parameter, the second to the second, and so on.

def show_interface(name, ip, desc):
    print("interface " + name)
    print(" description " + desc)
    print(" ip address " + ip + " 255.255.255.0")

show_interface("GigabitEthernet0/1", "10.10.1.1", "LAN-USERS")

Real output, checked in the lab terminal.

interface GigabitEthernet0/1
 description LAN-USERS
 ip address 10.10.1.1 255.255.255.0

Now see what happens when the order is wrong. Python does not know that "LAN-USERS" is a description; it only counts positions.

def show_interface(name, ip, desc):
    print("interface " + name)
    print(" description " + desc)
    print(" ip address " + ip + " 255.255.255.0")

show_interface("LAN-USERS", "GigabitEthernet0/1", "10.10.1.1")

Real output, checked in the lab terminal.

interface LAN-USERS
 description 10.10.1.1
 ip address GigabitEthernet0/1 255.255.255.0

No error appears, yet the config is nonsense. This kind of silent mistake is the dangerous one. The cure is the next idea.

Keyword arguments: say the name

You can write parameter=value in the call. Then the order no longer matters and the line explains itself.

def show_interface(name, ip, desc):
    print("interface " + name)
    print(" description " + desc)
    print(" ip address " + ip + " 255.255.255.0")

show_interface(desc="LAN-CCTV", name="GigabitEthernet0/2", ip="10.10.2.1")

Real output, checked in the lab terminal.

interface GigabitEthernet0/2
 description LAN-CCTV
 ip address 10.10.2.1 255.255.255.0

A good habit for functions with three or more parameters is to use keyword arguments in the call. Anyone reading the line knows what each value means.

Default values: optional parameters

Most LAN interfaces use 255.255.255.0. You can give a parameter a default with = in the def line. If the caller does not supply that argument, the default is used.

def show_interface(name, ip, mask="255.255.255.0"):
    print("interface " + name)
    print(" ip address " + ip + " " + mask)

show_interface("GigabitEthernet0/1", "10.10.1.1")
show_interface("GigabitEthernet0/2", "10.10.2.1", "255.255.255.252")

Real output, checked in the lab terminal.

interface GigabitEthernet0/1
 ip address 10.10.1.1 255.255.255.0
interface GigabitEthernet0/2
 ip address 10.10.2.1 255.255.255.252

The first call uses the default mask. The second overrides it. Parameters with defaults must come after the ones without defaults.

Wrong number of arguments

If you give too few or too many arguments, Python stops and tells you exactly what is missing.

def show_interface(name, ip, desc):
    print("interface " + name)

show_interface("GigabitEthernet0/1", "10.10.1.1")

Real output, checked in the lab terminal.

Traceback (most recent call last):
  File "missing.py", line 4, in <module>
    show_interface("GigabitEthernet0/1", "10.10.1.1")
TypeError: show_interface() missing 1 required positional argument: 'desc'

The last line names the function and the missing parameter. In the lab this exact error appears whenever a call and a def disagree, so read the last line before anything else.

Common mistakes. (1) Mixing up parameter and argument: parameter is the name in def, argument is the value in the call. (2) Giving arguments in the wrong order and getting silent nonsense. (3) Putting a default parameter before a non-default one, which is a SyntaxError. (4) Using a number where text is needed: "vlan " + 10 fails; write "vlan " + str(10) or an f-string.

Exam trap. The words are tested directly: "name, ip in the def line are _; the values in the call are _". Parameters, then arguments. Also remember that keyword arguments may be given in any order.

The VLAN that went to the wrong switch

A script called add_vlan(switch, vlan_id, name) was used by two teammates. One wrote add_vlan("SW1", 20, "SCANNERS"); the other wrote add_vlan("SCANNERS", "SW1", 20) from memory. The second call produced a config for a switch named SCANNERS. Nothing crashed, because every value was text. The team changed all calls to keyword form, add_vlan(switch="SW1", vlan_id=20, name="SCANNERS"), and the mistake never came back.

Lesson: keyword arguments make calls self-documenting and safe against order mistakes.

"What is the difference between a parameter and an argument?"

A parameter is the variable name in the function definition. An argument is the actual value passed in when the function is called. Mention positional versus keyword arguments, and that default values make a parameter optional.

Key takeaways

  • Parameters are the named blanks in the def line; arguments are the values in the call.
  • Positional arguments are matched by order; keyword arguments by name.
  • mask="255.255.255.0" gives a parameter a default, so it becomes optional.
  • A wrong number of arguments gives a TypeError that names what is missing.
  • Use keyword arguments when a function has three or more parameters.
04

return: handing a result back (and building the config function)

So far our functions only showed text with print(). Real automation needs functions that hand a value back so the next line of the script can use it: save it to a file, send it to a router, or compare it with something. That is the job of return.

print() shows, return gives back

print() writes on the screen and the value is then gone. return sends the value to the line that called the function. Compare:

def double_print(n):
    print(n * 2)

def double_return(n):
    return n * 2

double_print(21)
answer = double_return(21)
print("The answer is", answer)
print("Plus one:", answer + 1)

Real output, checked in the lab terminal.

42
The answer is 42
Plus one: 43

double_print(21) showed 42 but nothing was kept. double_return(21) gave 42 back and we stored it in the variable answer, then used it again in the next lines. The moment a function runs return, it stops and leaves, so any line after return inside that function never runs.

Calleranswer = double_return(21)Functionn * 2 = 42return42 goes backCalleranswer holds 42

return carries the value to the caller

What you get when there is no return

If a function reaches its last line without a return, Python hands back a special value called None, which means "nothing here". This is the number one beginner bug in this module.

def build_line(ip):
    line = "ip address " + ip + " 255.255.255.0"

result = build_line("10.10.1.1")
print(result)
print(type(result))

Real output, checked in the lab terminal.

None
<class 'NoneType'>

The function built the text, but it never returned it, so result is None. Now see the error this causes when you try to use that None as a list:

def build_lines(name):
    lines = []
    lines.append("interface " + name)
    lines.append(" no shutdown")

cfg = []
cfg += build_lines("GigabitEthernet0/1")
print(cfg)

Real output, checked in the lab terminal.

Traceback (most recent call last):
  File "noreturn.py", line 7, in <module>
    cfg += build_lines("GigabitEthernet0/1")
TypeError: 'NoneType' object is not iterable

The last line says TypeError: 'NoneType' object is not iterable. Translate it: "you tried to loop or add something that is None". Whenever you see NoneType, ask yourself first: did my function forget return?

Returning a list of config lines

For automation we usually return data, not printed text. A list of lines is perfect: you can print it, save it to a file, or push it to a router. Here is the function from lab 1, finished, and called three times. "\n".join(cfg) glues the list into one text with a new line between the items.

def build_intf_config(name, ip, desc):
    """Return the config lines for one interface as a list."""
    lines = []
    lines.append(f"interface {name}")
    lines.append(f" description {desc}")
    lines.append(f" ip address {ip} 255.255.255.0")
    lines.append(" no shutdown")
    return lines

cfg = []
cfg += build_intf_config("GigabitEthernet0/1", "10.10.1.1", "LAN-USERS")
cfg += build_intf_config("GigabitEthernet0/2", "10.10.2.1", "LAN-CCTV")
cfg += build_intf_config("GigabitEthernet0/3", "10.10.3.1", "LAN-WIFI")
print("\n".join(cfg))
print("Lines built:", len(cfg))

Real output, checked in the lab terminal.

interface GigabitEthernet0/1
 description LAN-USERS
 ip address 10.10.1.1 255.255.255.0
 no shutdown
interface GigabitEthernet0/2
 description LAN-CCTV
 ip address 10.10.2.1 255.255.255.0
 no shutdown
interface GigabitEthernet0/3
 description LAN-WIFI
 ip address 10.10.3.1 255.255.255.0
 no shutdown
Lines built: 12

Walk through it: the text in triple quotes right under the def is a docstring, a one-line description that tools and humans can read. lines = [] starts an empty list. Each append adds one config line (the f"..." strings drop the parameter values into the text). return lines hands the finished list back. In the script, cfg += ... joins each returned list onto the big list. Three calls times four lines gives 12 lines, which is the number you will meet again in the lab.

Variables inside a function stay inside

A variable created inside a function (like lines) is local: it exists only while the function runs. The caller cannot see it.

def make_line():
    secret_line = "no shutdown"
    return secret_line

make_line()
print(secret_line)

Real output, checked in the lab terminal.

Traceback (most recent call last):
  File "scope.py", line 6, in <module>
    print(secret_line)
NameError: name 'secret_line' is not defined

The function ran fine, but the name secret_line does not exist outside it. To use a result, capture the returned value: x = make_line(). This keeps functions independent, which is exactly why they are safe to reuse.

Common mistakes. (1) Writing print(...) where return was needed: the lab script fails with the NoneType error. (2) Putting code after return and expecting it to run. (3) Writing return with the value on the next, un-indented line. (4) Forgetting to store the returned value: calling build_intf_config(...) alone throws the result away.

Exam trap. "What does the function return if it has no return statement?" None. "What is type(None)?" NoneType. Also: return ends the function immediately.

The generator that produced twelve blank lines

An engineer in the lab ran intf.py and got TypeError: 'NoneType' object is not iterable on line 16. The function printed correctly when tested alone, which made him suspect the list. A teammate told him to look at the end of the function. The last line was a comment saying "give the list back", not the statement. He typed return lines, the script built all twelve lines, and the push to R1 succeeded.

Lesson: a NoneType error almost always means a missing return.

"What is the difference between print and return in a function?"

print only displays text for a human and the value is lost. return gives the value back to the caller so it can be stored, tested, saved to a file or sent to a device. A function without return returns None. In automation I return data and print only at the edge of the script.

Key takeaways

  • return value hands a result back and ends the function.
  • No return means the caller receives None.
  • TypeError: 'NoneType' object is not iterable usually means a missing return.
  • Return data (a list of config lines), not printed text, so it can be reused.
  • Variables made inside a function are local; capture results with x = function().
05

Files: save results and read data with "with open"

A script forgets everything when it ends. If you want to keep a backup, produce a report for your manager, or read a list of routers that somebody else prepared, you need files. This chapter shows the three things you do with a file: write, append and read.

Why files matter in automation

Think of the script as a worker with no memory. A file is the notebook the worker writes in. In lab 1 the script writes branch.cfg, and a second script, push.py, reads that same file and sends it to router R1. The two scripts never talk to each other directly; the file is the hand-over point. This idea, keeping data in files and logic in code, appears again in the YAML and JSON modules.

Writing a file

open(name, mode) opens a file. The mode "w" means write. The safe way to open a file is with open(...) as f: followed by an indented block.

cfg = [
    "interface GigabitEthernet0/1",
    " description LAN-USERS",
    " ip address 10.10.1.1 255.255.255.0",
    " no shutdown",
]

with open("branch.cfg", "w") as f:
    f.write("\n".join(cfg) + "\n")

print("Saved", len(cfg), "lines")

Real output, checked in the lab terminal.

Saved 4 lines

What happened, in plain words: open("branch.cfg", "w") created a file called branch.cfg in the current folder (or emptied it if it already existed). as f gives the open file the short name f. f.write(text) puts text into it. When the indented block ends, with closes the file for you, even if an error happened inside. Always use with; forgetting to close a file can lose data.

Notice + "\n". The two characters backslash and n mean "new line". f.write() does not add a new line by itself, so you add it. Without it, all lines would stick together into one long line.

Reading a file back

Without a mode, open reads. f.read() returns the whole file as one string. .splitlines() cuts that string into a list with one item per line, with the new-line characters already removed.

with open("branch.cfg", "w") as f:
    f.write("interface GigabitEthernet0/1\n description LAN-USERS\n no shutdown\n")

with open("branch.cfg") as f:
    lines = f.read().splitlines()

print(lines)
print("Read", len(lines), "lines")
for line in lines:
    print("->", line)

Real output, checked in the lab terminal.

['interface GigabitEthernet0/1', ' description LAN-USERS', ' no shutdown']
Read 3 lines
-> interface GigabitEthernet0/1
->  description LAN-USERS
->  no shutdown

You can also loop over the file directly, one line at a time. Each line still ends with a new-line character, so strip() removes it:

with open("routers.txt", "w") as f:
    f.write("10.99.0.1\n10.99.0.2\n10.99.0.3\n")

with open("routers.txt") as f:
    for line in f:
        print("router:", line.strip())

Real output, checked in the lab terminal.

router: 10.99.0.1
router: 10.99.0.2
router: 10.99.0.3

Modes at a glance

ModeMeaningExisting content
"r" (default)readuntouched; error if the file does not exist
"w"writeerased first
"a"appendkept; new text goes at the end

Appending: a growing log

Use "a" when you want to add to the end without deleting what is already there, such as an audit log that grows with every run.

for router in ["R1", "R2"]:
    with open("audit.log", "a") as f:
        f.write(router + " checked\n")

with open("audit.log") as f:
    print(f.read())

Real output, checked in the lab terminal.

R1 checked
R2 checked

Reading a file that does not exist

with open("missing.txt") as f:
    data = f.read()

Real output, checked in the lab terminal.

Traceback (most recent call last):
  File "nofile.py", line 1, in <module>
    with open("missing.txt") as f:
FileNotFoundError: [Errno 2] No such file or directory: 'missing.txt'

FileNotFoundError means the name or the folder is wrong. In the lab terminal, ls shows the files in the current folder and cat file prints one. Check the spelling and the folder first.

Sending the file to a router

In lab 1, push.py reads branch.cfg and sends the lines to R1 over SSH. This code uses the lab library nkmiko (a lab version of the well-known Netmiko library), so the lab terminal runs it against R1 but a plain Python install would not.

Reference code (not run in the simulator).

from nkmiko import ConnectHandler

with open("branch.cfg") as f:
    lines = f.read().splitlines()
print("Read", len(lines), "lines from branch.cfg")

conn = ConnectHandler(device_type="nk_ios", host="10.99.0.1", username="netops", password="NK@2026")
conn.send_config_set(lines)
conn.save_config()
conn.disconnect()
print("Done - R1 configured")

This is what the lab terminal printed for it, and what you should see too after python3 push.py:

Read 12 lines from branch.cfg
Done - R1 configured

Twelve lines is three interfaces times four lines. If the number is not 12, your branch.cfg is wrong; fix the script that writes it, not push.py.

Common mistakes. (1) Opening with "w" when you meant "a", which erases the old log. (2) Forgetting "\n", so every line is glued together. (3) Not using with. (4) Reading a file in the wrong folder and getting FileNotFoundError.

Exam trap. "w" truncates an existing file; "a" appends. read() gives one string; readlines() and splitlines() give lists. f.write() needs a string, not a list.

The log that kept disappearing

A team wrote an audit script that saved the daily results with open("audit.log", "w"). Every morning the manager complained that only the last router was in the log. The cause was mode "w": each run erased the file and wrote it fresh. Changing one letter to "a" kept the history.

Lesson: know your mode before you run a script on a file that matters.

"How do you read a text file safely in Python?"

Use with open("file") as f: so the file closes automatically, then f.read().splitlines() for a list of lines or loop over f. Mention that mode "w" erases the file and "a" appends, and that a missing file raises FileNotFoundError.

Key takeaways

  • with open(name, mode) as f: opens a file and closes it automatically.
  • "w" erases and writes, "a" appends, no mode (or "r") reads.
  • f.write() adds no new line; add "\n" yourself.
  • f.read().splitlines() turns a file into a list of lines.
  • A missing file raises FileNotFoundError.
06

Errors, exceptions and how to read a traceback

Every programmer meets errors every day. The difference between a beginner and an experienced engineer is not that the experienced one avoids errors; it is that the experienced one reads the message and knows where to look. This chapter teaches you to read Python's error report, the traceback, in about ten seconds.

Two kinds of problems

  • A syntax error means Python could not even understand the line (a missing colon, a missing bracket). Python refuses to start the script and points at the line.
  • An exception is an error that happens while the script runs: the file is missing, the router does not answer, a dictionary key does not exist. Python creates an *exception object* with a type and a message, and unless somebody catches it, the script stops and prints a traceback.

A traceback, line by line

Here is a small script where one function calls another, and the inner one fails. The function connect pretends that a router did not answer by using raise, which is how code announces an exception on purpose.

def connect(ip):
    raise TimeoutError("TCP connection to device failed: " + ip)

def get_uptime(ip):
    connect(ip)
    return "up 3 days"

for ip in ["10.99.0.1", "10.99.0.2"]:
    print(get_uptime(ip))

Real output, checked in the lab terminal.

Traceback (most recent call last):
  File "tbnest.py", line 9, in <module>
    print(get_uptime(ip))
  File "tbnest.py", line 5, in get_uptime
    connect(ip)
  File "tbnest.py", line 2, in connect
    raise TimeoutError("TCP connection to device failed: " + ip)
TimeoutError: TCP connection to device failed: 10.99.0.1

Do not read it from the top. Read it from the bottom up, in three steps.

  1. The last line says what happened: the exception class (TimeoutError), a colon, and the message. This is usually 80 percent of the answer.
  2. The line above it (and the File ... line N entries) say where. The deepest entry, line 2, in connect, is the line that raised the error.
  3. The entries above are the chain of callers that led there: the loop at line 9 called get_uptime, which called connect. Read them upward to see who started it.

Newer Python versions add little ^^^^ marker lines under some code lines. They only point at the exact spot; the logic of reading is the same.

1. Last lineclass and message2. Deepest File linewhere it broke3. Lines abovewho called it

Read a traceback from the bottom

The traceback from the lab

In lab 2, check.py logs in to three routers with the lab library. This is the real traceback the lab terminal printed when the router at 10.99.0.2 did not answer:

Traceback (most recent call last):
  File "check.py", line 19, in <module>
    up = get_uptime(ip)
  File "check.py", line 10, in get_uptime
    conn = ConnectHandler(device_type="nk_ios", host=ip, username="netops", password="NK@2026")
nkmiko.exceptions.NetmikoTimeoutException: TCP connection to device failed.

Common causes of this problem are:
1. Incorrect hostname or IP address.
2. Wrong TCP port.
3. Intermediate firewall blocking access.

Device settings: nk_ios 10.99.0.2:22
(connection timed out)

Reading it: the class is NetmikoTimeoutException, so there was no SSH session at all. The Device settings line even names the IP, 10.99.0.2. The File entries show check.py line 19 called get_uptime, and line 10 inside it tried to connect. Notice that this tells you the script is fine and the network path to R2 is the problem.

The common exceptions

ExceptionWhat it usually means
NameErrorA typo in a variable or function name, or used before it exists
TypeErrorThe wrong kind of value, or wrong number of arguments, or a None that should be a list
KeyErrorAsked a dictionary for a key it does not have
IndexErrorAsked a list for a position that does not exist
FileNotFoundErrorOpened for reading a file that is not there
ValueErrorRight type, wrong content, such as int("abc")
NetmikoTimeoutExceptionNo SSH connection: device down, wrong IP, interface shut, ACL, SSH not enabled
NetmikoAuthenticationExceptionConnected, but the username or password was refused

Seeing a few for yourself

device = {"name": "R1", "role": "core"}
print(device["name"])
print(device["mgmt_ip"])

Real output, checked in the lab terminal.

R1
Traceback (most recent call last):
  File "keyerr.py", line 3, in <module>
    print(device["mgmt_ip"])
KeyError: 'mgmt_ip'

KeyError: 'mgmt_ip' tells you the key you asked for. Compare it with the keys that exist. Typos in key names are the most common cause.

vlans = [10, 20, 30]
print(vlans[3])

Real output, checked in the lab terminal.

Traceback (most recent call last):
  File "indexerr.py", line 2, in <module>
    print(vlans[3])
IndexError: list index out of range

A list of three items has positions 0, 1 and 2, so position 3 does not exist: IndexError: list index out of range.

A short debugging routine

  1. Run the script and read the last line.
  2. Find the deepest File line that belongs to your file and open that line.
  3. Print the variables on that line (print(ip, name)) to see what they really hold.
  4. Ask: is it my code or the world (network, file, credentials)?

Common mistakes. (1) Reading from the top and getting lost in the chain. (2) Fixing the line where the traceback ends when the real cause is a wrong value created earlier. (3) Copying an error into a search engine without the class name and message. (4) Treating a timeout as a script bug, when the device is simply unreachable.

Exam trap. A timeout is a connectivity problem; an authentication exception is a credential problem. Both look like "the script failed", so the exam asks you to separate them from the exception class.

The password nobody changed

A nightly backup script began failing for 4 of 40 routers. The traceback on all four ended in NetmikoAuthenticationException, not a timeout. The engineer ignored the network entirely, checked the credentials, and found that those four routers still had the old local password after a rotation project. Ten minutes later the backup was complete.

Lesson: the exception class tells you which layer to investigate before you touch anything.

"How do you read a Python traceback?"

I start at the bottom: the exception class and message say what happened. Then I look at the deepest File line in my own code to see where. The lines above show the chain of function calls. For a Netmiko timeout I check reachability; for an authentication exception I check credentials.

Key takeaways

  • Syntax errors stop the script before it starts; exceptions happen while it runs.
  • Read a traceback from the bottom: class and message first, then where, then who called.
  • The exception class points to a layer: script, data, file, network or credentials.
  • KeyError, IndexError and TypeError are the most common beginner exceptions.
  • Timeout means no connection; authentication means connected but refused.
07

try / except: keep going when one router fails

In a real network some device is always down, rebooting, or has a changed password. A script that crashes on router 2 never reaches routers 3 to 50. try/except lets you say: "try this; if this particular error happens, do that instead, and carry on".

The idea with a simple example

Dividing by zero is an exception we can create on demand, so it is a safe way to see the structure. The code under try: is the risky part. If an exception happens there, Python jumps to the matching except block instead of stopping.

def share(total, people):
    return total / people

for people in [4, 0, 2]:
    try:
        print("Each gets", share(100, people))
    except ZeroDivisionError:
        print("Cannot share among", people, "people - skipping")

print("Script finished")

Real output, checked in the lab terminal.

Each gets 25.0
Cannot share among 0 people - skipping
Each gets 50.0
Script finished

Walk through it. For 4 people the division works and prints 25.0. For 0 people Python raises ZeroDivisionError; the try block stops at that line, the except block runs, and the loop continues with the next value. The last line proves the script was never killed.

try:risky code runsno errorcontinue after the blockerrorjump to exceptnext itemloop goes on

try / except flow

Handling a dead router in a loop

Now the same pattern for devices. To keep this example runnable anywhere, get_uptime here is a stand-in that fails for one address. Replace it with a real login function in the lab.

def get_uptime(ip):
    if ip == "10.99.0.2":
        raise TimeoutError("no SSH session to " + ip)
    return "uptime is 3 days"

results = []
for ip in ["10.99.0.1", "10.99.0.2", "10.99.0.3"]:
    try:
        up = get_uptime(ip)
        results.append(f"{ip} OK {up}")
    except TimeoutError:
        results.append(f"{ip} FAILED unreachable")

for row in results:
    print(row)

Real output, checked in the lab terminal.

10.99.0.1 OK uptime is 3 days
10.99.0.2 FAILED unreachable
10.99.0.3 OK uptime is 3 days

All three routers appear in the report. The dead one is recorded as FAILED instead of killing the script. This is the exact pattern of lab 2.

The pieces you can add

  • Several excepts. Different problems, different messages. Python runs the first one that matches.
  • as e. except TimeoutError as e: gives the error object the name e. print(e) shows its message and type(e).__name__ its class name.
  • Several classes at once. except (TimeoutError, PermissionError):.
  • else. Runs only if there was no error. finally. Runs always, error or not; good for "close the connection".
def risky(n):
    if n == 1:
        raise TimeoutError("device did not answer")
    if n == 2:
        raise PermissionError("login refused")
    return "ok"

for n in [0, 1, 2]:
    try:
        value = risky(n)
    except TimeoutError as e:
        print(n, "timeout:", e)
    except PermissionError as e:
        print(n, "denied:", type(e).__name__, "-", e)
    else:
        print(n, "worked:", value)
    finally:
        print(n, "done")

Real output, checked in the lab terminal.

0 worked: ok
0 done
1 timeout: device did not answer
1 done
2 denied: PermissionError - login refused
2 done

Catch specific errors, not everything

A bare except: catches every error, including your own typos, and hides them. Watch what happens when a typo (reslt) hides inside a try with a bare except:

results = []
for ip in ["10.99.0.1", "10.99.0.3"]:
    try:
        reslt.append(ip)
    except:
        results.append(ip + " FAILED")
print(results)

Real output, checked in the lab terminal.

['10.99.0.1 FAILED', '10.99.0.3 FAILED']

Both routers are reported as FAILED, but the network is perfectly fine; the real bug is a NameError caused by a typo, and the bare except swallowed it. Catch only the exceptions you expect, so real bugs still produce a traceback.

Avoiding the exception in the first place

Some errors are better prevented. A dictionary's .get() returns None (or a default you choose) instead of raising KeyError:

device = {"name": "R1"}
print(device.get("vlan"))
print(device.get("vlan", "no vlan set"))

Real output, checked in the lab terminal.

None
no vlan set

The real lab code

For lab 2 the stand-in is replaced by SSH calls through the lab library. This is reference code for the lab, wrapped as you will write it.

Reference code (not run in the simulator).

from nkmiko.exceptions import NetmikoTimeoutException, NetmikoAuthenticationException

for ip in ROUTERS:
    try:
        up = get_uptime(ip)
        results.append(f"{ip} OK {up}")
    except NetmikoTimeoutException:
        results.append(f"{ip} FAILED unreachable")
    except NetmikoAuthenticationException:
        results.append(f"{ip} FAILED login")

Remember: handling the error is not fixing the network. After the script reports FAILED, find the root cause and repair it.

Common mistakes. (1) A bare except: that hides typos. (2) Putting too much code in try, so you no longer know which line failed. (3) Forgetting to indent except at the same level as try. (4) Catching an error, printing nothing, and never telling anyone a device was skipped. Always record it.

Exam trap. finally runs whether or not an error occurred, else only when there was none. Python checks the except blocks in order and runs the first match, so put specific classes before general ones.

The silent skip

A configuration-audit script wrapped everything in try/except: pass. For three weeks it reported no problems. When the auditors checked, two of the 30 routers had never been reached: the credentials were wrong and the script quietly skipped them. After that the team logged every skipped device with its exception class, and the report ended with a count of OK and FAILED.

Lesson: an except block must record what it caught, never hide it.

"Why is a bare except a bad idea?"

It catches every exception, including programming mistakes such as typos and unexpected conditions, so bugs become silent. I catch the specific classes I expect, for example the Netmiko timeout and authentication exceptions, log them, and let everything else surface as a traceback.

Key takeaways

  • try: holds the risky lines; except SomeError: runs only if that error happens.
  • The loop carries on with the next device after an except block.
  • as e gives the error object; finally always runs; else runs only without error.
  • Catch specific exceptions; a bare except hides your own bugs.
  • Record every failure; handling an error does not fix the network.
08

Troubleshooting workflow: the script that met a dead router

This chapter walks through lab 2 the way a working engineer would. The ticket reads: "check.py logs in to the three Kaveri Foods branch routers and saves each one's uptime to results.txt. Today it crashes halfway and results.txt is never written." There are two jobs: make the script robust, and fix the network problem behind the crash. Doing only the first one is the classic mistake.

The workflow in five steps

  1. Reproduce. Run the script and keep the full output.
  2. Read the traceback from the bottom. Class, message, then the deepest File line.
  3. Decide the layer. Script bug, bad data, file problem, credentials, or network?
  4. Protect the script. Add try/except for the expected failure so one bad device cannot stop the rest.
  5. Fix the cause and verify. Repair the device, run again, and read the result file.
Reproducerun the scriptReadbottom of tracebackLayerscript or networkProtecttry / exceptFix + verifyresults.txt

Troubleshooting loop

Step 1 and 2: reproduce and read

The script (lab file check.py) defines get_uptime(ip), which opens an SSH session, runs show version | include uptime, and returns the line. The loop calls it for 10.99.0.1, 10.99.0.2 and 10.99.0.3. Running it produces the traceback you met in the tracebacks chapter. These are the lines that matter:

AUTO> python3 check.py
Traceback (most recent call last):
  File "check.py", line 19, in <module>
    up = get_uptime(ip)
  File "check.py", line 10, in get_uptime
    conn = ConnectHandler(device_type="nk_ios", host=ip, username="netops", password="NK@2026")
nkmiko.exceptions.NetmikoTimeoutException: TCP connection to device failed.
...
Device settings: nk_ios 10.99.0.2:22
(connection timed out)

Two answers are already visible: the exception class is NetmikoTimeoutException and the router it was reaching when it crashed is 10.99.0.2. The script never reached 10.99.0.3 and never wrote results.txt, because the crash happened before the with open("results.txt", "w") block.

Step 3: which layer?

A timeout means "no SSH session". The script's logic is fine for router 1 (it got past it). So test the network path from the automation host with ping:

AUTO> ping 10.99.0.2
host (10.99.0.2) not reachable
host (10.99.0.2) not reachable
host (10.99.0.2) not reachable
host (10.99.0.2) not reachable
host (10.99.0.2) not reachable

Not even a ping works, so this is a network problem on or near R2, not a coding problem.

Step 4: protect the script

Lines 18 to 20 of check.py are the risky ones. The lab hint wraps them like this (use nl check.py to see line numbers, then edit and insert to change lines):

Reference code (not run in the simulator).

for ip in ROUTERS:
    try:
        up = get_uptime(ip)
        results.append(f"{ip} OK {up}")
    except NetmikoTimeoutException:
        results.append(f"{ip} FAILED unreachable")

Running it again now finishes, writes the file, and shows exactly which device is the problem:

AUTO> python3 check.py
10.99.0.1 OK KF-PUNE uptime is 0 minutes
10.99.0.2 FAILED unreachable
10.99.0.3 OK KF-NASHIK uptime is 0 minutes
Saved 3 rows to results.txt

Note the hostname of the router at 10.99.0.3: it is KF-NASHIK, taken from the uptime is line. This is a good sign that the script now collects what it should from the working routers.

The same logic works on any machine. Here is the pattern with a stand-in login function, so you can see the report logic on its own:

def get_uptime(ip):
    if ip == "10.99.0.2":
        raise TimeoutError("no SSH session")
    return "uptime is 0 minutes"

results = []
for ip in ["10.99.0.1", "10.99.0.2", "10.99.0.3"]:
    try:
        results.append(f"{ip} OK {get_uptime(ip)}")
    except TimeoutError:
        results.append(f"{ip} FAILED unreachable")

ok = sum(1 for r in results if " OK " in r)
print("\n".join(results))
print("OK:", ok, "FAILED:", len(results) - ok)

Real output, checked in the lab terminal.

10.99.0.1 OK uptime is 0 minutes
10.99.0.2 FAILED unreachable
10.99.0.3 OK uptime is 0 minutes
OK: 2 FAILED: 1

Step 5: fix the real cause and verify

The ticket is not finished. try/except only hid the problem. Look at the router:

R2# show ip interface brief
Interface              IP-Address      OK? Method Status                Protocol
GigabitEthernet0/0     10.99.0.2       YES manual administratively down down
GigabitEthernet0/1     unassigned      YES unset  administratively down down
GigabitEthernet0/2     unassigned      YES unset  administratively down down
GigabitEthernet0/3     unassigned      YES unset  administratively down down

The management interface GigabitEthernet0/0 is administratively down: somebody ran shutdown on it. Bring it back:

R2# configure terminal
R2(config)# interface GigabitEthernet0/0
R2(config-if)# no shutdown
R2(config-if)# end

Wait a few seconds for the link to come up, run the script again, and read the file with cat results.txt:

AUTO> python3 check.py
10.99.0.1 OK KF-PUNE uptime is 0 minutes
10.99.0.2 OK KF-NAGPUR uptime is 0 minutes
10.99.0.3 OK KF-NASHIK uptime is 0 minutes
Saved 3 rows to results.txt

Three OK lines and no FAILED means the fix worked. The try/except stays in the script, so the next outage will be reported instead of crashing it.

Which exception, which first move

Exception in the tracebackFirst move
NetmikoTimeoutExceptionping the IP, check the interface status, ACLs, SSH enabled
NetmikoAuthenticationExceptionCheck username, password, AAA, and whether the account is locked
TypeError ... NoneTypeLook for a missing return
KeyErrorCompare the key you asked for with the keys that exist
FileNotFoundErrorls the folder; check the name and the current directory

Common mistakes. (1) Declaring victory after adding try/except while the router is still down. (2) Catching the wrong exception class, so the crash continues. (3) Putting the with open("results.txt", "w") block inside the loop, which erases earlier rows each time. (4) Not re-running the script after the fix and trusting that it works.

Exam trap. The correct order is: find the cause from the traceback, protect the script, fix the network, then verify. If an answer says "wrap everything in try/except and close the ticket", it is wrong.

Eleven routers OK, one silent

A weekly compliance script reported 11 routers OK and one FAILED unreachable. A less careful engineer might have ignored the single line. The on-call engineer pinged the router, saw it was down, and found the management interface shut by a failed change window the night before. Without the FAILED row nobody would have noticed for weeks, because the router still forwarded user traffic.

Lesson: good error handling turns a hidden fault into a visible line in a report.

"A script crashes on the third of twenty devices. How do you handle it?"

I read the traceback to see the exception class and the device. I check the network path to that device with ping and the interface status. In the script I wrap the per-device work in try/except for the specific exceptions, record FAILED with the reason, and let the loop continue. Then I fix the real fault and re-run to verify.

Key takeaways

  • Reproduce, read the traceback, decide the layer, protect, fix, verify.
  • A timeout points to the network; try ping and the interface status.
  • try/except keeps the script alive; it never repairs the device.
  • Keep the file-writing block outside the loop so earlier rows are not erased.
  • Always re-run after the fix and read the result file.
09

Summary and exam checklist

You have gone from copy-pasted config lines to a reusable function, from forgotten output to files on disk, and from a script that dies on router 2 to one that reports it and carries on. This last chapter collects everything in one place for revision.

Can-do checklist

Tick each item only if you can do it without looking:

  • Write a function with def, indent its body by 4 spaces, and call it.
  • Explain parameters versus arguments, and use positional, keyword and default arguments.
  • Use return to hand a value back, and explain why a missing return gives None.
  • Build a list of config lines in a function and join it with "\n".join(...).
  • Open a file with with open(...) in "w", "a" and read modes, and use splitlines().
  • Read a traceback from the bottom: class, message, deepest File line.
  • Write try / except SpecificError so a loop continues after a failure.
  • Explain why a bare except: is dangerous.
  • Separate "my script is wrong" from "the network is down", and do both fixes.

Mini glossary

function
A named block of code that you write once and call many times.
def
The keyword that defines a function.
parameter
The named blank in the def line, such as name.
argument
The actual value passed when you call the function.
return value
The result a function hands back to its caller; None if there is no return.
docstring
A triple-quoted description right under the def line.
local variable
A variable that exists only inside the function that created it.
with open
Opens a file and closes it automatically at the end of the block.
exception
An error object created while the script runs.
traceback
The report of the call chain that led to an uncaught exception.
try / except
Catches an exception so the script can carry on.
finally
A block that always runs, error or not.

Syntax cheat-sheet

TaskCode
Define a functiondef build(name, ip="10.0.0.1"):
Call with keywordsbuild(name="Gi0/1", ip="10.10.1.1")
Give a value backreturn lines
Write a file (erases)with open("a.txt", "w") as f: then f.write(text + "\n")
Append to a filewith open("a.txt", "a") as f:
Read lineslines = f.read().splitlines()
Handle a failuretry: ... except TimeoutError as e:
Safe dictionary lookupd.get("key", "default")

Most tested facts

  • Defining a function does not run it; calling it does.
  • A function without return returns None, and the usual symptom is a NoneType TypeError.
  • print shows, return gives back.
  • Mode "w" erases an existing file; "a" appends; f.write() adds no new line.
  • Read a traceback from the last line upward.
  • NetmikoTimeoutException is a connectivity problem; NetmikoAuthenticationException is a credential problem.
  • Catch specific exceptions; a bare except hides your own bugs.
  • try/except handles an error in the script; it does not repair the device.

The two labs in one paragraph each

Lab 1, reusable functions. intf.py first crashes with TypeError: 'NoneType' object is not iterable because the function does not return lines. After you add the return, the description line and a third call for GigabitEthernet0/3, the script prints 12 lines, saves them in branch.cfg, and push.py sends the 12 lines to R1, where show ip interface brief lists 10.10.1.1, 10.10.2.1 and 10.10.3.1.

Lab 2, a dead router. check.py crashes with NetmikoTimeoutException at 10.99.0.2. You wrap the risky lines in try/except so the run finishes and writes results.txt with the router at 10.99.0.3 (KF-NASHIK) OK and 10.99.0.2 FAILED, then find the shut GigabitEthernet0/0 on R2, run no shutdown, and re-run until all three routers are OK.

What comes next

The next module, data formats, shows how to store device lists and variables in JSON and YAML instead of typing them into the script. After that you will parse CLI output into data. Everything you learned here, functions, files and try/except, appears in every one of those scripts.

From a one-off script to a team tool

A network team started with a 40-line script that only the author understood. After refactoring it into three functions (get_devices, check_device, write_report), adding try/except around the device check, and saving results to a file, a colleague could run it on the first day. When a new vendor was added, only check_device changed.

Lesson: functions, files and error handling are what turn a personal script into a shared tool.

"Walk me through how you would make a collection script production-ready."

I split it into small functions with clear parameters and return values, read the device list from a file, wrap the per-device work in try/except for the specific exceptions, record each failure with its reason, write a results file, and end with a count of OK and FAILED. Then I test with one device deliberately down.

Key takeaways

  • Functions: define once, call many times, pass values in, return a result.
  • Files: with open in "w", "a" and read mode; splitlines() for lists.
  • Tracebacks: bottom first; class tells you the layer.
  • try/except: specific exceptions, record failures, loop continues.
  • Fix the network cause as well as protecting the script, then verify.
🎓 For educational purposes only — all devices are simulationsTerms of UsePrivacy Policy© 2026 Network Kings
CONFIG by Network Kings — an educational IT simulation platform for learning purposes only. It is not Cisco IOS, Junos, FortiOS or PAN-OS and contains no Cisco, Juniper, Fortinet or Palo Alto Networks software. Cisco, IOS, CCNA, CCNP, Juniper, JNCIA, JNCIS, JNCIP, Fortinet, FortiGate, FortiOS, NSE, Palo Alto Networks, PAN-OS and PCNSE are trademarks of their respective owners. Network Kings is not affiliated with or endorsed by Cisco Systems, Inc., Juniper Networks, Inc., Fortinet, Inc. or Palo Alto Networks, Inc.