By the end you'll know
- How to write a function with
def, and call it - How to pass values in, and hand an answer back with
return - Why
returnandprint()are not the same thing - How to catch an error with
tryandexcept, and carry on - How to read a traceback that runs through more than one function
Where your checker is
At the end of Lesson 3, your checker loops over a list of sites and reports each one. It works, but two things will hurt as it grows.
First, the loop does everything itself: looks up the status code, works out milliseconds, decides up or down, prints, and counts. When the checker later needs any of those steps somewhere else, like retrying a site or saving results, you'd have to copy them, and every bug would need fixing in several places.
Second, real data is messy. If one site in a list of 300 has a status code that isn't a number, the checker crashes, and the sites after it never get checked.
This lesson fixes both. Functions package steps under a name, so you write them once. try and except let the script deal with a bad value and keep going. You'll change checker.py in two steps again.
Functions: write it once, use it anywhere
A recipe card is written once, and from then on you just say "let's make pancakes" instead of reciting the steps. A function is the same: a set of steps with a name, so the rest of your script can use them by name.
You make a function with def (short for define), a name, brackets, and a colon. The indented lines under it are the function's body:
def to_ms(seconds):
return round(seconds * 1000)
seconds is a parameter: a name for whatever value is handed in when the function is used, the way a recipe can be made for 2 people or for 6. Writing def doesn't run anything yet. It just teaches Python a new name. The body runs when you call the function, by writing its name with a value in the brackets:
print(to_ms(0.25))
print(to_ms(1.8))
250
1800
The value you pass in, 0.25, is called an argument. Inside the function, seconds holds it. You've been calling functions since Lesson 1: print(), len(), round() and int() are all functions that come with Python. Now you can make your own.
A function can take more than one parameter, separated by commas, and indentation marks what's inside, exactly as with if and loops. Variables you create inside a function stay inside it; the rest of the script can't see them.
return: handing an answer back
A calculator shows its answer on the screen, and that's the end of it. A spreadsheet formula puts its answer in a cell, where other formulas can use it. print() is the calculator: it shows a value to a person. return is the spreadsheet cell: it hands the value back to your code, so the next line can use it.
That's why you can write response_ms = to_ms(0.25) and get 250 stored in response_ms. When a function reaches return, it stops and gives that value back to whoever called it.
A function that only prints, and never returns, hands back None, Python's "no value" from Lesson 2. Here's one, with what it shows when it runs:
def greet(name):
print(f"Hello, {name}")
answer = greet("Kwame")
print(answer)
Hello, Kwame
None
The greeting appeared, because print() ran inside the function. But answer is None, because nothing was returned. If you want to use a function's result, it needs a return.
A function can have more than one return. Whichever one Python reaches first ends the function, and the lines after it don't run.
Now the first change to the checker. In checker.py, add these two functions between the list of sites and print("Checking sites..."), with two blank lines around each one (that's just Python's style convention, for readability):
def is_up(status_code):
return status_code >= 200 and status_code < 400
def to_ms(seconds):
return round(seconds * 1000)
Then, inside the loop, change two lines so they use them:
response_ms = to_ms(site["response_seconds"])
if is_up(status_code):
Run the checker. The output is exactly the same as before, and that's the point: the script does the same job, but each rule now has a name. is_up() answers "is this code a good one?" and returns a bool. to_ms() converts seconds. If the rule for "up" ever changes, there's one place to change it.
Notice the order: the functions are defined above the loop that calls them. Python reads top to bottom, so a function has to be defined before the line that calls it runs. Putting all the defs near the top is the usual habit.
try and except: a backup plan
When a road is closed, a satnav doesn't give up on the journey; it finds a way around and carries on. try and except give your script the same kind of backup plan, so one bad value doesn't stop a check of 300 sites.
You met ValueError in Lesson 2: int("two hundred") can't work, so Python stops. An error like that is called an exception, and you can catch it:
text = "n/a"
try:
number = int(text)
except ValueError:
number = 0
print(number)
0
Python runs the lines under try. If they work, it skips the except block. If one of them raises a ValueError, Python jumps straight to except ValueError:, runs those lines instead, and carries on below as if nothing happened. No traceback, no crash.
Always name the error you expect, like except ValueError:. You might see except: on its own, which catches every error, but that also hides the mistakes you didn't expect, like a typo in a variable name, and turns them into silent wrong answers. Catch what you know how to handle, and let everything else show up.
Now the second change. Lesson 5 will read the list of sites from a file, and as Lesson 2 showed, data from a file arrives as text. So make the data look like that now. In the list of sites, put quotes around each status code, so 200 becomes "200", and add a fourth site whose status code isn't a number at all:
{"name": "example.edu", "status_code": "n/a", "response_seconds": 0.0},
Then add a third function, below to_ms(). It checks one site from start to finish, using the other two, and returns a word: "up", "down" or "skipped":
def check_site(site):
name = site["name"]
try:
status_code = int(site["status_code"])
except ValueError:
print(f"SKIP {name}: status code isn't a number")
return "skipped"
response_ms = to_ms(site["response_seconds"])
if is_up(status_code):
print(f"UP {name}: {status_code} in {response_ms} ms")
return "up"
print(f"DOWN {name}: {status_code} in {response_ms} ms")
return "down"
int() turns the text into a number. For "n/a", it raises a ValueError, the except block prints a SKIP line, and return "skipped" ends the function early, so the lines below it never run for that site.
Last, replace everything from down = 0 to the end of the file with a much shorter loop, which calls check_site() and counts the answers:
down = 0
skipped = 0
for site in sites:
result = check_site(site)
if result == "down":
down = down + 1
elif result == "skipped":
skipped = skipped + 1
print(f"{down} of {len(sites)} sites down, {skipped} skipped")
Reading the loop now tells you what the script does; the function tells you how.
Checkpoint: your checker so far
Your checker.py should now look like this. If your blank lines differ a little, that's fine, but line numbers in the break-it section assume this layout:
# A website checker, built one piece at a time.
sites = [
{"name": "example.com", "status_code": "200", "response_seconds": 0.25},
{"name": "example.org", "status_code": "503", "response_seconds": 1.8},
{"name": "example.net", "status_code": "301", "response_seconds": 0.4},
{"name": "example.edu", "status_code": "n/a", "response_seconds": 0.0},
]
def is_up(status_code):
return status_code >= 200 and status_code < 400
def to_ms(seconds):
return round(seconds * 1000)
def check_site(site):
name = site["name"]
try:
status_code = int(site["status_code"])
except ValueError:
print(f"SKIP {name}: status code isn't a number")
return "skipped"
response_ms = to_ms(site["response_seconds"])
if is_up(status_code):
print(f"UP {name}: {status_code} in {response_ms} ms")
return "up"
print(f"DOWN {name}: {status_code} in {response_ms} ms")
return "down"
print("Checking sites...")
print(f"{len(sites)} sites to check")
down = 0
skipped = 0
for site in sites:
result = check_site(site)
if result == "down":
down = down + 1
elif result == "skipped":
skipped = skipped + 1
print(f"{down} of {len(sites)} sites down, {skipped} skipped")
Run it:
python3 checker.py
Checking sites...
4 sites to check
UP example.com: 200 in 250 ms
DOWN example.org: 503 in 1800 ms
UP example.net: 301 in 400 ms
SKIP example.edu: status code isn't a number
1 of 4 sites down, 1 skipped
The bad site was reported, the good ones were all checked, and the summary is honest about what was skipped. That's the behaviour you want from any script that handles real data.
Break it on purpose
Make each mistake, run the checker, and put it back before the next.
Forget to return. In to_ms(), delete the word return, leaving just round(seconds * 1000):
Checking sites...
4 sites to check
UP example.com: 200 in None ms
DOWN example.org: 503 in None ms
UP example.net: 301 in None ms
SKIP example.edu: status code isn't a number
1 of 4 sites down, 1 skipped
No error, and "None ms". The function still did the maths, then threw the answer away, so it handed back None. Whenever you see None where you expected a value, check the function that produced it for a missing return. Put return back.
Call it without what it needs. On line 26, change to_ms(site["response_seconds"]) to to_ms(), with nothing in the brackets:
Checking sites...
4 sites to check
Traceback (most recent call last):
File "checker.py", line 40, in <module>
result = check_site(site)
File "checker.py", line 26, in check_site
response_ms = to_ms()
TypeError: to_ms() missing 1 required positional argument: 'seconds'
This traceback has two File lines, because the error happened inside a function that was called from somewhere else. Read it from the bottom, as always: the last line says to_ms() was called without its seconds argument. The File line just above it, line 26 in check_site, is where the mistake is. The one above that, line 40, is the call that led there. Each File line is one step in the chain of calls, with the most recent at the bottom. Put the argument back.
Catch the wrong error. On line 22, change except ValueError: to except KeyError::
Checking sites...
4 sites to check
UP example.com: 200 in 250 ms
DOWN example.org: 503 in 1800 ms
UP example.net: 301 in 400 ms
Traceback (most recent call last):
File "checker.py", line 40, in <module>
result = check_site(site)
File "checker.py", line 21, in check_site
status_code = int(site["status_code"])
ValueError: invalid literal for int() with base 10: 'n/a'
The first three sites were checked, then the whole run crashed on the fourth. except KeyError: only catches KeyErrors, and int("n/a") raised a ValueError, so nothing caught it. An except only helps if it names the error that actually happens, which is why the last line of the traceback, the error's name, is the first thing to read. Change it back to ValueError, and run the checker once more to see 1 of 4 sites down, 1 skipped.
Where you are now
Your checker now has a named function for each job, and one bad value no longer stops the run. But the sites are still typed into the script itself, and the results vanish when it finishes. Lesson 5 reads the list of sites from a file, and saves what the checker found.
Check yourself
What happens when Python reads def to_ms(seconds): and its body?
to_ms() uses print() instead of return. What does response_ms = to_ms(0.25) store?
In def to_ms(seconds):, what is seconds?
int("n/a") runs inside a try, with except ValueError: below it. What happens?
Why name the error, as in except ValueError:, instead of catching every error?
A traceback shows two File lines: line 40, then line 26 below it. Where did it fail?
Key terms
- Function: a named block of steps, made with
def, that runs when you call it. - Parameter: the name in a function's brackets, like
seconds, that holds what's passed in. - Argument: the value you pass when calling a function, like
0.25. - return: ends a function and hands a value back to the code that called it.
- Exception: an error raised while a script runs, like ValueError or KeyError.
- try and except: run some lines, and if they raise the named error, run a backup plan instead of crashing.
- Call chain: the functions that called each other on the way to an error, shown as one File line each in a traceback.
Go deeper
- Defining functions (opens in a new tab) in the official tutorial.
- Errors and exceptions (opens in a new tab) covers
try,exceptand more ways to handle errors.