IntermediatePython · 20 min · 18 challenges

Python Foundations: Intermediate

A new engineer at Dropbox, where the desktop sync client has been written in Python for years, pastes a 20-line script that counts files in a folder and runs it. The terminal answers with 4 lines that start with Traceback. Whether she reads those lines as noise or as a map to the exact failing line decides whether she ships today or spends the afternoon guessing.

Indentation and Code Blocks

Daily Life
Interviews
In most programs, some lines run only when a condition holds, some run again and again, and some wait until they are called by name. Python decides which lines belong to such a group by looking at how far each line is indented. A header line ends with a colon, and every line indented beneath it is part of the header's block. The block ends at the first line that steps back to the header's own indentation.
balance = 120
price = 150
if price > balance:
print("not enough money")
print("short by", price - balance)
print("checkout finished")

if runs its block only when the expression after it is True. Here price > balance is True, so both indented prints run, and then line 6 runs because it was never inside the block. Swap the numbers so the condition is False and the indented lines are skipped, while line 6 still runs. Now watch what one change in spacing does to the same 6 lines.

balance = 200
price = 150
if price > balance:
print("not enough money")
print("short by", price - balance)
print("checkout finished")
>>>Output
short by -50
checkout finished
Line 5 lost its 4 spaces, so it left the block. It now runs on every checkout and reports a negative shortfall to a customer who has plenty of money. The text of the program changed by 4 characters and the program's meaning changed completely, which is why Python treats indentation as part of the code.
Multiple Choice

What does this program print?

score = 40
if score > 50:
print("pass")
score = score + 10
print(score)

The 3 headers you will meet most

Each ends with a colon and owns the indented lines under it
  • if price > 100: runs its block once, and only when the expression is True
  • for i in range(5): runs its block once per value, with i holding the current one each pass
  • def total(price): names its block and runs none of it now; the block runs each time total(...) is called later

Each header gets a lesson of its own later. What matters here is the shape they share: a colon, then an indented block, then a return to the left margin. Press play on the loop below and watch the single indented line run 5 times while the header hands i a new value on each pass.

1for i in range(5):
2 print(i)
i=
output0 lines
i takes 0, item 1 of range(5)
1/12

A def block differs in timing. When the file runs past line 1, Python stores the 2 indented lines under the name calculate_total and runs none of them. They run on line 5, when the name is called with 100 and 0.08, and the result 108.0 travels back to be stored in result and printed.

1def calculate_total(price, tax_rate):
2 total = price * (1 + tax_rate)
3 return total
4
5result = calculate_total(100, 0.08)
6print(result)
result=
output0 lines
defines calculate_total
1/7

Blocks inside blocks

A block can hold another header, which opens a deeper block one level further in. Each level is, by convention, 4 more spaces; Python requires only that every line of one block is indented the same amount. In the program below, ask of every line which header owns it. Lines 4 and 5 sit 8 spaces in, so they belong to the if, which itself belongs to the for. Line 6 sits 4 spaces in, belongs to the for alone and runs on every pass. Line 7 is back at the margin and runs once, after the loop has finished all 4 passes.

1count = 0
2for i in range(4):
3 if i > 1:
4 count = count + 1
5 print("counted", i)
6 print("checked", i)
7print("total", count)
count=
i=
output0 lines
count starts at 0
1/20

Lines at 8 spaces run only when i > 1; the line at 4 spaces runs every pass; the margin line runs once.

Indentation errors that stop the program

Python does not guess at a block's borders, so 3 mistakes stop the program before any line runs. A header with nothing indented under it, and a line indented where no header asked for it, each raise an IndentationError. Indenting some lines of one block with a tab and others with spaces raises a TabError, because a tab and 4 spaces look identical on screen and differ to the interpreter. Each report names the line, so open the file at that line and count the spaces.
What you wroteErrorMessage
if 3 > 2: followed by print("yes") at the marginIndentationErrorexpected an indented block
print("hi") pushed in 4 spaces after a plain line, with no header above itIndentationErrorunexpected indent
one line of a block indented with a tab, the other lines of that block with spacesTabErrorinconsistent use of tabs and spaces in indentation
✓Do
  • Indent with 4 spaces per level, every time
  • End every header line with a colon
  • Set your editor to insert spaces when you press Tab
  • Return to the margin to close a block
✗Don't
  • Mix tabs and spaces in one file
  • Indent a line that no header asked for
  • Use 2 spaces in one block and 4 in another
  • Expect a blank line to end a block; only dedenting does

Practice

Importing Modules

Daily Life
Interviews

Python ships with hundreds of modules, each a file of code written and tested for you: square roots, random numbers, dates, text in JSON form. Python loads a module only when you ask for it. The import statement finds the module's file, runs it once, and binds the module's name in your program so you can reach what it holds with a dot.

import math
print(math.sqrt(144))
print(math.pi)
print(math.floor(7.8))
>>>Output
12.0
3.141592653589793
7

Read math.sqrt as the sqrt that lives inside math. After line 1 the name math refers to the module, and the dot looks inside it for a name. sqrt returns a float even for a perfect square, pi is a plain float with 15 digits after the point, and floor drops the fraction and hands back an int. Leave out line 1 and the name math means nothing to the program.

print(math.sqrt(144))
The program stops on its first line with a NameError, the same error a misspelled variable produces. The module exists on disk, but nothing in this program has named it yet. Section 3 reads traceback lines in full; for now, a NameError on a module's name means a missing import.

2 ways to import

import math brings in the whole module under one name, and every use carries the prefix. from math import sqrt, pi copies those 2 names into your program, so you call sqrt(81) with no prefix. The second form reads shorter, at the cost of hiding where sqrt came from when someone reads line 40 of your file.

from math import sqrt, pi
print(sqrt(81) * pi)
>>>Output
28.274333882308138
✓import math
  • One name is added to your program: math
  • Every use carries the prefix: math.sqrt(2)
  • The origin of each name is visible at the call
  • Safe when 2 modules both define a sqrt
✗from math import sqrt
  • The name sqrt is added directly
  • Calls read shorter: sqrt(2)
  • A reader scrolls to the top to find the origin
  • A later from cmath import sqrt silently replaces it
TIP
Put every import at the top of the file, one per line. A reader then sees in the first few lines everything the program depends on, and Python loads each module once no matter how many lines import it.
Multiple Choice

Which line fails when this program runs, and with what error?

from math import sqrt
print(sqrt(16))
print(math.floor(2.7))

A tour of the standard library

ModuleWhat it holdsOne name inside
mathsquare roots, rounding, constantsmath.sqrt(x)
randomrandom numbers and random choicesrandom.randint(1, 6)
datetimedates, times and the gaps between themdatetime.date.today()
jsonturning JSON text into Python values and backjson.loads(text)
osthe operating system: paths, folders, environmentos.getcwd()
timepausing, and measuring elapsed secondstime.sleep(2)

Each of these is installed with Python itself, so import json works on any machine that has Python and nothing else. Naming a module that does not exist, or misspelling one, fails at the import line with a ModuleNotFoundError rather than somewhere later in the program.

import maths

Importing your own file

A module is any file ending in .py, including the ones you write. Save the constants and helpers that 2 scripts share in a file named pricing.py, and both scripts can import pricing. The module's name is the filename without .py, and Python looks for it first in the folder the running script lives in.

TAX_RATE = 0.08
def with_tax(price):
return price * (1 + TAX_RATE)
import pricing
print(pricing.TAX_RATE)
print(pricing.with_tax(100))

Importing pricing runs every line of pricing.py once, top to bottom, which is how TAX_RATE gets its value and with_tax gets defined. A print at the top level of pricing.py would run at import time too, which surprises people the first time a bare import produces output. Keep a module's top level to constants and definitions, and the importing script decides what gets printed.

Practice

Reading Input From the User

Daily Life
Interviews

Every program so far had its values written into the code. input() lets the person running the program supply a value while it runs. The call shows its prompt text, pauses, waits for the user to type and press Enter, and then hands back whatever was typed. The program resumes on the next line with that text stored wherever you assigned it.

name = input("Your name: ")
print("Hello,", name)

The prompt is ordinary text, so end it with a space or the cursor sits jammed against the colon. Whatever comes back is a string, every time. Type 42 and input returns "42", 2 characters that happen to be digits. Press Enter with nothing typed and it returns "", the empty string. Spaces typed before or after the value survive as well.

User typesValue storedType
Maya"Maya"str
42"42"str
3.5"3.5"str
(Enter, nothing typed)""str
7 " 7 "str

Adding a number to typed text

Treating that string as a number is the most frequent mistake in a first program with input. The program below stands in for a session where the user typed 30 at an Age prompt; the variable holds exactly what input would have returned, so the result is the same as the live version.

age = "30"
print(age + 1)

Python refuses because + between a string and an int has no single meaning: join them, or add them? The message says which reading it took, concatenation, and what it could not accept, the int. Before any arithmetic, convert the text to a number with int() or float(). Each is a function that reads a string of digits and returns the matching numeric value.

age = "30"
age = int(age)
print(age + 1)
height = float("1.75")
print(height * 2)
>>>Output
31
3.5

int() accepts digits with an optional leading sign and ignores spaces around them, so int(" 7 ") returns 7. float() accepts those plus a decimal point, so float("3") returns 3.0. Text that isn't a number, such as letters or, for int(), a decimal point, makes the conversion fail with a ValueError, the signal that the type was right and the content was wrong.

print(int(" 7 "))
print(float("3"))
print(int("ten"))

The first 2 lines printed before line 3 failed, because a program runs every line up to the one that raises. int("3.5") fails the same way, since a decimal point is not a digit; convert through float() when the user may type a fraction.

Multiple Choice

The user types 7 at the prompt. What does this program print?

count = input("How many? ")
print(count * 3)

Convert at the moment of reading

The usual shape wraps the input call directly in the conversion, so the variable never holds the raw string and no later line can forget to convert it.

price = float(input("Price: "))
quantity = int(input("Quantity: "))
total = price * quantity
print("Total:", total)
Price typedQuantity typedResult
2.504Total: 10.0
19.992Total: 39.98
52Total: 10.0
abc(never asked)ValueError raised at line 1
2.501.5ValueError raised at line 2

The fourth row shows the program dying at line 1, so the Quantity prompt never appears. The last row shows that int() rejects a fraction even though a human would read 1.5 as a number. Checking the text before converting, and asking again when it fails, uses tools from a later lesson; for now the rule is that the conversion line is where bad input surfaces.

What input() guarantees
  • ▸It always returns a str, for digits and for an empty line alike
  • ▸It drops the Enter keypress; the newline is never part of the value
  • ▸It keeps every other character typed, spaces included
  • ▸It blocks: nothing after it runs until the user presses Enter

Practice

Reading a Traceback

Daily Life
Interviews
When a line raises an error that nothing handles, Python stops and prints a traceback: a report of where the program was when it failed. A new programmer sees a wall of text. The wall has 4 parts, and only 2 of them matter on a first read: the last line and the line above it.
price = 100
total = price * 1.08
print(totl)
Traceback lineWhat it tells you
Traceback (most recent call last):A report follows; the frames are ordered so the one that failed comes last
File "main.py", line 3, in <module>The file and the line number; <module> means top level, outside any def
print(totl)The source text of that line, copied so the problem can be spotted without opening the file
NameError: name 'totl' is not definedThe error's name, a colon, then its message: what went wrong in Python's words
Read it bottom up. The last line names the kind of failure and says which name was missing. The line above shows the code, where the misspelling is visible. Only then is the line number needed, to go and fix it. The first line is a fixed header and carries nothing specific to this failure.

The error name picks the category

The error's name sorts the failure into a category, and each category points at a different kind of fix. The message after the colon narrows it to the specific name, type or value involved.
Error namePython is sayingThe usual cause
NameErrorthis name has never been assigneda typo, a use above the assignment, or a missing import
TypeErrorthis operation does not work on these typesa str where a number was needed, often from input()
ValueErrorthe type is right and the content is wrongint("ten") or int("3.5")
ZeroDivisionErrorthe right side of / was 0a count or a total that turned out empty
ModuleNotFoundErrorno module with this name was founda misspelled import, or a file in the wrong folder
IndentationErrorthe block structure cannot be parseda missing or an extra level of indentation

More than one frame

When the failing line sits inside a def block, the traceback shows 2 frames: the call that started things, then the line inside the function that raised. Most recent call last means the frame at the bottom is the one that was running when the error happened.

def average(total, count):
return total / count
print(average(10, 0))
Traceback lineFrame
Traceback (most recent call last):header
File "main.py", line 4, in <module>frame 1: the call
print(average(10, 0))frame 1 source
File "main.py", line 2, in averageframe 2: inside average
return total / countframe 2 source
ZeroDivisionError: division by zerothe error

The bottom frame says line 2 divided by zero. The frame above explains why count was 0: line 4 passed it in. The fix belongs at line 4, or at whatever produced the 0, rather than at line 2, which did exactly what it was written to do. A traceback marks where the error surfaced; the cause can be any line that ran earlier.

The reported line and the real cause
A TypeError on line 9 that adds rate to a float may trace back to line 2, where rate was assigned the string "0.08" instead of the number 0.08. Start at the reported line, then follow each name in it upward to the line where it got its value, and keep going until you find the value that doesn't match its use.
Multiple Choice

What does the last line of this program's traceback say, and which line holds the mistake? Names assigned at the top of the file are visible inside the def.

rate = "0.08"
price = 200
def with_tax(amount):
return amount * (1 + rate)
print(with_tax(price))

Errors with no traceback header

A SyntaxError or IndentationError appears before the program runs a single line, because Python reads the whole file first and couldn't parse it. The report skips the Traceback header, shows the file and line, the offending source, a caret under the point where parsing gave up, and the error name. There's no frame to list, since nothing was executing. A traceback with the header always means the file parsed and some line actually ran.

Practice

Trying Code in the Interactive Shell

Daily Life
Interviews

Running python with a filename executes a script: Python reads the file, runs it top to bottom, and exits. Running python with no filename opens the interactive shell. It shows a >>> prompt, runs each line the moment you press Enter, and waits for the next one. Everything in this lesson works in both, with one difference that changes how each is used.

You type at >>>The shell shows
2 + 35
total = 10 * 4(nothing; an assignment has no value to show)
total40
"ab" * 3'ababab'
print(total)40
import math(nothing)
math.sqrt(2)1.4142135623730951

The shell prints the value of any expression you type, with no print call, and shows a string with quotes around it so that '40' and 40 can be told apart. A script does none of that. Put the same lines in a file and run it, and only the print line produces output.

2 + 3
total = 10 * 4
total
"ab" * 3
print(total)
>>>Output
40

Lines 1, 3 and 4 were computed and their results thrown away. In a script, output comes from print alone; in the shell, output also comes from the expression echo. A value you saw in the shell goes silent in a script until you wrap it in print.

Multiple Choice

You paste these 3 lines from a shell session into a file and run it as a script. What is printed?

import math
math.sqrt(49)
print(math.floor(7.0 / 2))

Blocks in the shell

A header line in the shell switches the prompt to ..., meaning a block is open. Type the indented lines, then press Enter on an empty line to close the block and run it. That blank line is what the shell needs, unlike a script, where dedenting alone closes a block.

PromptYou typeShell shows
>>>for i in range(3):
... print(i * 10)
...(empty line)0, then 10, then 20
>>>i2

In the last row, i still holds its final value after the loop, because the shell remembers every name you assign until you close it. Close the shell with exit() or Ctrl-D and all of it is gone, which is why anything worth keeping goes in a file.

Which one, when

✓Interactive shell
  • Checking what an expression evaluates to
  • Seeing what a function returns before relying on it
  • Looking around a module: dir(math) lists every name in it
  • Trying one line of a bigger script on its own
  • Reproducing a traceback with the exact values that caused it
✗Script
  • Anything you will run twice
  • Programs longer than a handful of lines
  • Work that reads input() or prints for a user
  • Code someone else will read or run
  • Anything that must survive closing the terminal

The 2 work together. Try an expression in the shell until it gives the value you want, paste that line into the script, and run the script. When a traceback points at a line, type that line into the shell with the same values to watch the failure up close. dir(math) and help(math.floor) answer most questions about a module faster than a search does.

Capstone

The capstone pulls the 5 sections into one short script: an import, an indented block, a value standing in for user input, and a bug that produces a traceback. Predict the traceback first, then decide the fix.
Multiple Choice

The variable typed stands in for what input() returned. Where does this script fail, and what fixes it?

import math
typed = "12.25"
if typed > 0:
root = math.sqrt(typed)
print("root:", root)
print("done")
A traceback reports only the first failure, because the program stops there. Fix that line and rerun, and the program gets further, which can expose a second bug of another kind, such as a print whose indentation keeps it from ever running.
❯❯❯PUTTING IT ALL TOGETHER

> An analyst at Dropbox writes a 30-line script that asks for a folder name and a minimum age in days, imports datetime, and prints every file in that folder older than the minimum, then a summary count. The first run prints a traceback ending in TypeError, because the minimum age typed at the second prompt was compared to an int while still a str.

The traceback's bottom frame points at the comparison line, and following the name upward lands on the second input() call, the one asking for the minimum age, which returned a string; int() belongs on that call.
After the fix, the summary line prints once per file instead of once, because the summary print was indented into the loop; moving it back to the margin runs it a single time.
Opening the shell, importing datetime and typing datetime.date.today() shows what the module returns before the analyst relies on it inside the script.
Every import sits at the top of the file, so the next person to open it sees in 2 lines that the script depends on datetime and os and nothing else.
KEY TAKEAWAYS
A colon opens a block and indentation decides which lines belong to it; 4 spaces moved can change what a program does.
import loads a module's file once and binds its name; math.sqrt reads as the sqrt inside math, and a missing import shows up as a NameError.
input() always returns a str; int() or float() turns it into a number, and a ValueError at that line means the user typed something that isn't one.
Read a traceback from the bottom: the error name and message, then the failing line, then the frames above it to find where the bad value came from.
The shell echoes every expression and forgets everything on exit; a script prints only what print asks for and is the home of anything you will run again.

Blocks, imports, input, tracebacks and the shell: the 5 habits that turn lines of Python into a program you can run.

Category
Python
Difficulty
intermediate
Duration
20 minutes
Challenges
18 hands-on challenges

Topics covered: Indentation and Code Blocks, Importing Modules, Reading Input From the User, Reading a Traceback, Trying Code in the Interactive Shell

Lesson Sections

  1. Indentation and Code Blocks (concepts: Conditionals)

    In most programs, some lines run only when a condition holds, some run again and again, and some wait until they are called by name. Python decides which lines belong to such a group by looking at how far each line is indented. A header line ends with a colon, and every line indented beneath it is part of the header's block. The block ends at the first line that steps back to the header's own indentation. Line 5 lost its 4 spaces, so it left the block. It now runs on every checkout and reports a

  2. Importing Modules (concepts: Iteration)

    The program stops on its first line with a NameError, the same error a misspelled variable produces. The module exists on disk, but nothing in this program has named it yet. Section 3 reads traceback lines in full; for now, a NameError on a module's name means a missing import. 2 ways to import A tour of the standard library Importing your own file Practice

  3. Reading Input From the User (concepts: Functions)

    Adding a number to typed text Convert at the moment of reading Practice

  4. Reading a Traceback (concepts: Functions)

    When a line raises an error that nothing handles, Python stops and prints a traceback: a report of where the program was when it failed. A new programmer sees a wall of text. The wall has 4 parts, and only 2 of them matter on a first read: the last line and the line above it. Read it bottom up. The last line names the kind of failure and says which name was missing. The line above shows the code, where the misspelling is visible. Only then is the line number needed, to go and fix it. The first l

  5. Trying Code in the Interactive Shell (concepts: Scope)

    Blocks in the shell Which one, when Capstone The capstone pulls the 5 sections into one short script: an import, an indented block, a value standing in for user input, and a bug that produces a traceback. Predict the traceback first, then decide the fix. A traceback reports only the first failure, because the program stops there. Fix that line and rerun, and the program gets further, which can expose a second bug of another kind, such as a print whose indentation keeps it from ever running.