◀ Course contents Part 3 · Module 3-05

Modules & the Standard Library

Most of what you need is already written

Python ships with a library that covers dates, randomness, statistics, JSON, file paths, regular expressions and a great deal more. Knowing what is in it is the difference between forty lines and one — and the forty-line version is usually the one with the bug in it, because the standard library's has been tested by rather more people than you.

Ready?

1

Three Ways to Bring Something In

import math                  # the whole module
math.sqrt(144)

from math import sqrt, ceil  # just these names
sqrt(144)

import statistics as stats   # under a shorter name
stats.mean(scores)

The first form keeps the module's name in front of everything, which is usually what you want: math.sqrt says where it came from, and two modules can both have a sqrt without any argument about it.

from is right when a name is used constantly and is unmistakable — from datetime import date, and every line below is shorter for it. It is wrong when the bare name could be anything: from json import load gives you a load that tells the next reader nothing.

The alias form is for long names, and by convention only where the whole community uses the same abbreviation. Inventing your own is how code becomes unreadable to everyone but its author.

Never from x import *

It pulls in every public name the module has, so you no longer know what is defined where — and a later import can silently replace a name an earlier one gave you. The error, when it comes, is a function behaving oddly rather than anything that names the import.

Quick check

Which import reads best for a name used once, halfway down a long file?

2

The Ones Worth Knowing by Name

math

sqrt, ceil, floor, pi. ceil and floor always round the same direction, unlike round().

statistics

mean, median, stdev. The median is the one people mean when they say average and get a wrong answer.

random

randint, choice, shuffle, sample. seed() makes it repeatable, which is how you test anything that uses it.

datetime

date, timedelta. Subtracting two dates gives a duration; adding a duration gives a date. Never do calendar arithmetic by hand.

json

loads for text to Python, dumps for the other way. The s means "string" — the versions without it work on files.

collections

Counter tallies in one line; defaultdict removes the "is this key there yet" check.

Counter is worth dwelling on, because it replaces something you have now written twice:

counts = Counter(levels)          # the whole get(key, 0) + 1 loop
counts.most_common(3)             # the three commonest, with their counts

And json.loads turns text into exactly the dictionaries and lists from Part 2 — which is why that module spent so long on nested lookups.

Quick check

How many days between two dates?

3

Your Own Code Is a Module Too

Any .py file is a module. Put functions in pricing.py and another file can say:

import pricing
pricing.line_total(3, 1250)

from pricing import line_total
line_total(3, 1250)

Importing a module runs it, once. Every function definition happens, and so does every line that is not inside one — which is why a file that prints a report at the top level will print it the moment anyone imports it.

The guard that stops that is worth recognising on sight:

def main():
    ...

if __name__ == "__main__":
    main()

__name__ is "__main__" when the file is the one being run, and the module's own name when it is being imported. So the block runs when you execute the file and stays quiet when somebody imports it — the same file usable as a tool and as a library.

One idea per module

The same rule as functions, one level up. pricing.py, parsing.py, report.py — each one nameable in a phrase. A file called utils.py is where code goes when nobody decided what it was, and it grows until nobody can say what it contains.

Imports go at the top of the file, standard library first, in one block. Not scattered through the body where the reader has to go hunting.

Quick check

What does if __name__ == "__main__": achieve?

4

Three Ways an Import Goes Wrong

Shadowing a module with a file. Name your own file random.py and every import random in that directory finds yours instead of the standard library's. The error is AttributeError: module 'random' has no attribute 'randint', which points at the wrong thing entirely.

The same happens with a variable. Once you write math = 5, the name math is that number, and math.sqrt is an AttributeError on an integer.

Importing what you did not mean. import datetime gives you the module; from datetime import datetime gives you the class inside it, which happens to have the same name. Both are common, and mixing them up produces TypeError: 'module' object is not callable.

Circular imports. Two modules that import each other. Python is part way through defining the first when it starts on the second, which asks for something from the first that does not exist yet. The fix is never a clever import — it is noticing that the two files want to be one, or that a third one should hold what they share.

AttributeError

math = 5
math.sqrt(9)

The name is a number now. The module is still loaded and no longer reachable.

Fine

max_value = 5
math.sqrt(9)

Never name a variable after a module you are using.

Quick check

A file in your project is called json.py. What happens to import json elsewhere in that directory?

0 of 9 completed

Real Python runs right here in your browser — nothing to install, nothing sent to a server. The interpreter downloads once the first time you press Run, then stays cached.

01

To do

import math makes the module available under its own name, so every use says where it came from.

import math
math.sqrt(144) # 12.0
math.ceil(4.1) # 5
math.floor(4.9) # 4

ceil and floor always round the same direction, which is what you want when the question is "how many boxes" rather than "what is the nearest number".

Your task: print the square root of 144, the ceiling of 4.1, the floor of 4.9, and pi to four decimal places:

12.0
5
4
3.1416
your_code.py
Python
Hint

import math at the top. The last line is round(math.pi, 4) — or an f-string with a .4f spec, which would give the same characters here.

Output

      
    02

    To do

    from math import ceil, floor brings those two names into your file directly, so calls are shorter.

    This reads well when the name is unmistakable and used constantly. It reads badly when the bare name could have come from anywhere — a lone load() tells the next reader nothing.

    Your task: a warehouse packs items into boxes of 12. Import ceil and floor directly and work out how many boxes are needed for 100 items, and how many of those are full:

    9
    8

    Nine boxes are needed because the last four items still need one; eight of them are full.

    your_code.py
    Python
    Hint

    from math import ceil, floor at the top. ceil rounds up for the boxes needed, floor rounds down for the full ones.

    Output
    
          
      03

      To do

      import statistics as stats brings the module in under a name of your choosing.

      Worth doing for genuinely long module names, and worth doing only with the abbreviation everyone else uses — an alias nobody recognises is strictly worse than the full name.

      mean and median are not the same question. Five salaries of 30, 31, 32, 33 and 200 have a mean of 65 and a median of 32, and only one of those describes what most people earn.

      Your task: import statistics under the alias stats and print the mean and median of the scores, then of the salaries:

      76.25
      79.5
      65.2
      32
      your_code.py
      Python
      Hint

      import statistics as stats, then stats.mean(...) and stats.median(...). Notice how far apart the two salary figures are.

      Output
      
            
        04

        To do

        random gives you dice rolls, shuffles and samples. It also gives you code that behaves differently every run, which is impossible to test and unpleasant to debug.

        random.seed(n) fixes the starting point, so the same seed produces the same sequence every time. In real code you seed in the test and leave it alone in production.

        Your task: seed with 7, then print five dice rolls as a list and a shuffled copy of the queue. With that seed the answers are fixed:

        [3, 2, 4, 6, 1]
        ['test', 'notify', 'deploy', 'build']

        Seed once at the top, before either call — the rolls and the shuffle draw from the same sequence.

        your_code.py
        Python
        Hint

        random.seed(7) first. rolls is a comprehension over range(5) calling random.randint(1, 6). random.shuffle(queue) changes the list in place and returns None, so call it on its own line.

        Output
        
              
          05

          To do

          Months have different lengths, years have different lengths, and every piece of code that assumes otherwise fails eventually in a way nobody traces back.

          from datetime import date, timedelta
          d = date(2026, 9, 2)
          d + timedelta(days=30) # a date
          (later - earlier).days # a whole number of days

          Subtracting two dates gives a duration; .days takes the number out of it. Adding a timedelta gives a date back.

          Your task: from 2 September 2026, print the date itself, the date thirty days later, the number of days until Christmas, and the start date formatted as day, short month, year:

          2026-09-02
          2026-10-02
          114
          02 Sep 2026
          your_code.py
          Python
          Hint

          from datetime import date, timedelta. date(2026, 9, 2) builds the start and printing it gives the ISO form. The gap is (christmas - start).days, and the last line is start.strftime("%d %b %Y").

          Output
          
                
            06

            To do

            A JSON response is text. json.loads turns it into exactly the dictionaries and lists from Part 2, which is why that module spent so long on nested lookups.

            obj = json.loads(raw)      # text  -> Python
            text = json.dumps(obj) # Python -> text

            The s means "string". The versions without it read from and write to files, which is the next module.

            sort_keys=True on dumps makes the output stable, which matters the moment two runs are compared to each other.

            Your task: parse the response, print the quantity, the first tag, and how many tags there are, then print the record back out as text with sorted keys:

            3
            hot
            2
            {"name": "tea", "qty": 3, "tags": ["hot", "green"]}
            your_code.py
            Python
            Hint

            import json, then record = json.loads(raw). After that it is ordinary dictionary and list work. The last line is json.dumps(record, sort_keys=True).

            Output
            
                  
              07

              To do

              Counter does the whole counts[key] = counts.get(key, 0) + 1 loop from module 2-05, and adds a way to ask for the commonest entries.

              from collections import Counter
              counts = Counter(levels)
              counts.most_common(2) # [('INFO', 3), ('ERROR', 2)]

              It behaves like a dictionary everywhere it matters, with one useful difference: a key it has never seen reads as 0 rather than raising.

              Your task: tally the levels, then print the commonest two, the count of INFO, the count of a level that never appeared, and the number of distinct levels:

              [('INFO', 3), ('ERROR', 2)]
              3
              0
              3
              your_code.py
              Python
              Hint

              from collections import Counter, then counts = Counter(levels). most_common(2) takes an argument for how many you want, and counts["DEBUG"] is 0 rather than a KeyError.

              Output
              
                    
                08

                To do

                Collecting things into per-key lists needs a check every time: is there a list under this key yet? defaultdict answers it once, at creation.

                from collections import defaultdict
                groups = defaultdict(list)
                groups["eu"].append("tea") # the list is created on demand

                You hand it the function that makes a default — list, not list(). It calls it when a missing key is first touched.

                Your task: group the items by region, then print the groups as an ordinary dictionary, one region's list, and the number of regions:

                {'eu': ['tea', 'cup'], 'us': ['mat']}
                ['tea', 'cup']
                2
                your_code.py
                Python
                Hint

                groups = defaultdict(list) — pass the type itself, with no brackets. Then groups[region].append(item) works even the first time a region appears.

                Output
                
                      
                  09

                  To do

                  import math binds the name math to the module. Assign anything else to that name and the module is still loaded and no longer reachable through it.

                  The error — AttributeError: 'int' object has no attribute 'sqrt' — names the type rather than the mistake, which is why this can take longer to spot than it deserves. The same thing happens at file level: a file of your own called random.py makes the real one unreachable from anywhere in that directory.

                  Your task: run it, read the error, then rename the variable — leaving the import and the calculation alone — so it prints:

                  12.0
                  5
                  your_code.py
                  Python
                  Hint

                  The variable wants a name of its own. Call it something meaningful — per_box, say — and the two prints then need that name in the second one.

                  Output
                  
                        

                    The daily digest

                    To do

                    Every night the platform posts a JSON summary of the day's sessions, and every morning somebody turns it into four lines that fit in a chat message. Write that.

                    The payload is JSON text: a date, and a list of sessions each with a learner, a track and a number of minutes.

                    Use the standard library for all of it. Every figure below has a one-call answer, and a hand-rolled version of any of them fails a check.

                    • payload — the parsed JSON
                    • reported_on — the payload's date, as a real date object
                    • by_track — a Counter of sessions per track
                    • minutes_by_learner — a defaultdict(list) of every learner's session lengths
                    • median_minutes — the median session length across every session
                    • busiest — the track with the most sessions, as a plain string

                    Then print exactly four lines:

                    Digest for 02 Sep 2026
                    Sessions: 6 across 3 tracks
                    Busiest: python (3)
                    Median session: 35 min, longest learner total: 120

                    The date is formatted day, short month, year. The last figure is the highest total minutes any single learner accumulated.

                    your_code.py
                    Python
                    Hint

                    json.loads for the payload. date.fromisoformat(payload["date"]) gives a real date, and .strftime("%d %b %Y") formats it. Counter takes a list of every session's track — build that with a comprehension. defaultdict(list) and one loop for the learner totals. statistics.median over every session's minutes. by_track.most_common(1)[0] gives the busiest track and its count as a tuple. The longest learner total is max() over the sums of each learner's list.

                    Output
                    
                          

                      Notification