Chapter 13 of 24 · Python for Beginners — Complete Free Tutorial
Modules, Packages and pip
Every chapter of the Python for Beginners series so far has lived in a single file, which works beautifully until a file grows past a few hundred lines. Chapter 13 of this 24-chapter series introduces the three units Python programmers actually ship: modules (one .py file), packages (folders of modules with import rules) and pip, the installer that reaches the Python Package Index.
What a module is
A module is any .py file, and everything Python ships with — math, datetime, json — is a module too. Three import shapes cover daily work: import math loads a module you use through a prefix, from datetime import date pulls out one name, and import random as rnd renames it (the convention behind famous aliases such as numpy as np). In the REPL, typed lines start with >>> and the rest are Python’s replies:
>>> import math
>>> from datetime import date
>>> import random as rnd
>>> math.sqrt(144)
12.0
>>> date(2026, 10, 4)
datetime.date(2026, 10, 4)
Expected output: the reply lines 12.0 and datetime.date(2026, 10, 4). Prefer explicit imports over from module import *: they keep every name traceable to its source.
Writing your own module
Create greeter.py. Nothing marks a file as a module — existing is enough:
# greeter.py
def greet(name):
return f"Hello, {name}!"
if __name__ == "__main__":
print(greet("world"))
The last two lines are a main guard. Python sets a module’s __name__ to "__main__" when the file is executed directly and to the module’s own name when it is imported, so the guarded code runs only when you mean it to. The file can be run either way:
python3 greeter.py
python3 -m greeter
Expected output:
Hello, world!
Hello, world!
The first names the file; the second asks the module search path to find and run greeter as a script. The same trick explains two tiny files: demo.py containing print(f"__name__ is {__name__}") prints __name__ is __main__ when run directly, but __name__ is demo when a runner.py containing import demo pulls it in. Imported modules are cached, so that body runs once.
How Python finds your modules
Every import is answered by a search through sys.path, a list of folders visited in a fixed order. The first entry is the folder your script lives in — for python3 -c it is '', meaning the current working directory:
| Order | Where Python looks | What lives there |
|---|---|---|
| 1 | The script’s folder (or the working directory for -c) | Your own modules |
| 2 | Folders listed in the PYTHONPATH environment variable | Scratch and experimental code |
| 3 | The standard library | math, datetime, json |
| 4 | site-packages | Third-party code installed by pip |
When nothing matches, Python says exactly what it hunted for: ModuleNotFoundError: No module named 'not_a_module'. Note what that first entry implies — python3 scripts/tool.py searches scripts/, not the folder where you typed the command, which is the cause of most baffling import errors.
Packages: modules in folders
Once a project has a dozen modules they move into a folder, and the folder becomes a package. The marker is an __init__.py file, which may be empty but runs when the package is imported — the natural place to expose the public names:
myproject/
├── main.py
└── greeter/
├── __init__.py
├── casual.py
└── formal.py
# greeter/__init__.py
from .formal import greet as _formal_greet
from .casual import greet as _casual_greet
def greet(name, formal=True):
return _formal_greet(name) if formal else _casual_greet(name)
That leading dot makes it a relative import: “look inside this package”. Files outside the package use absolute imports, naming the package in full:
# main.py
from greeter import greet
if __name__ == "__main__":
print(greet("Ada"))
print(greet("Linus", formal=False))
Expected output: Good day, Ada. then Hey Linus!. Prefer relative imports inside a package so it can be moved freely, absolute ones outside it — run a package file as a script too early and you get ImportError: attempted relative import with no known parent package.
A tour of the standard library
Everything that arrived with the interpreter is the standard library: hundreds of modules you never install. os handles folders, paths and environment variables; sys describes the running interpreter; math gives roots and constants; random shuffles and samples; datetime handles dates and durations; json turns dictionaries into text and back:
import json, math, os, random, sys
from datetime import datetime, timedelta
print(f"Python {sys.version.split()[0]}, pi {math.pi:.3f}")
print(os.path.join("data", "reports", "q4.csv"))
random.seed(0)
print(random.sample(["a", "b", "c", "d", "e"], 3))
print((datetime(2026, 10, 4, 9, 30) + timedelta(days=100)).isoformat())
print(json.dumps({"name": "Ada", "langs": ["python", "sql"]}))
Expected output: the first line reports your own interpreter version, the rest are deterministic:
Python 3.13.12, pi 3.142
data/reports/q4.csv
['d', 'e', 'a']
2027-01-12T09:30:00
{"name": "Ada", "langs": ["python", "sql"]}
Installing other people’s code with pip
PyPI (the Python Package Index, pronounced “pie Py”) hosts hundreds of thousands of open-source projects. pip ships with Python, and the safe habit is to run it through the interpreter you are using, so packages land where your code will look for them:
python3 -m pip install requests
python3 -m pip show requests
Expected output: download lines are trimmed here; pip resolves and installs the dependencies too, then reports the package metadata (this is Python 3.13 with pip 25.3):
Collecting requests
Collecting charset_normalizer<4,>=2 (from requests)
Installing collected packages: urllib3, idna, charset_normalizer, certifi, requests
Successfully installed certifi-2026.7.22 charset_normalizer-3.5.2 idna-3.20 requests-2.34.2 urllib3-2.8.0
Name: requests
Version: 2.34.2
Summary: Python HTTP for Humans.
Installing gives you a working environment today; pinning gives you the same one tomorrow. pip freeze prints every installed package with its exact version, in the format requirements.txt expects:
python3 -m pip freeze > requirements.txt
cat requirements.txt
Expected output:
certifi==2026.7.22
charset-normalizer==3.5.2
idna==3.20
requests==2.34.2
urllib3==2.8.0
Commit that file: a teammate, or you on a fresh laptop, rebuilds the identical set with python3 -m pip install -r requirements.txt. That habit becomes reliable only once every project has its own environment — exactly what the next chapter builds.
Common mistakes
- Running a bare
pip— that executable can belong to a different Python than the one running your code, so packages install but never import; usepython3 -m pip. - Naming a file after a standard-library module — a local
json.pyoremail.pyshadows the real one and breaks imports in confusing ways; rename your file. from module import *— it hides where names came from and can silently overwrite your own variables; import the names you actually use.- Circular imports —
a.pyimportsb.pywhich importsa.py, leaving partially initialised modules andAttributeErrors; move shared code into a third module. - Assuming the working directory is where your script lives — imports resolve from the script’s folder first, so a helper beside your command can still be invisible.
Practice checklist
- Write
greeter.py, run it directly and withpython3 -m greeter, and confirm both printHello, world!. - Reproduce the
demo.py/runner.pypair and explain the two__name__values in one sentence each. - Build the
greeterpackage tree, runmain.py, then remove one dot from a relative import and read the error. - Combine
os,randomanddatetimein one script that prints a random date during 2026 — three standard-library modules, zero installs. - Install a package you genuinely want, read its metadata with
python3 -m pip show, and compare notes with the JavaScript modules chapter.
What's next?
Modules give you imports and pip gives you other people’s code, but installing into one shared interpreter means projects eventually break each other. Give every project a private interpreter in Virtual Environments and Project Layout.
Frequently Asked Questions
Python is a general-purpose language first released in 1991, designed around one idea: code is read far more often than it is written. English keywords and indentation instead of braces make it readable, the standard library covers files, JSON, dates and testing, and pip adds thousands more packages. The same language handles scripts, web APIs, data work and machine learning.
None at all. Python is widely considered the friendliest first language: no declaration keywords, no braces, no build step, and error messages that name the problem. This series begins at your first print() call. If you already read JavaScript or TypeScript you will adjust quickly, but no previous language is assumed anywhere in these 24 chapters.
Python 3 only, any release from 3.12 onwards. Python 2 reached end of life in 2021 and its print "hi" statements and xrange() no longer run. One historical quirk remains: installers ship the interpreter as python3 on macOS and Linux, while Windows usually offers python or the py launcher, and every command in this series works with that one-word swap.
More than you might expect. Everyday automation such as renaming files, cleaning spreadsheets or scraping pages; web backends with Django, Flask or FastAPI; data analysis and machine learning with pandas, NumPy and PyTorch; DevOps tooling, desktop apps and games. Python is also the classic glue language, calling C, Rust or Fortran libraries from short readable scripts.
Pure Python can run 10 to 100 times slower than C, yet that rarely blocks you: heavy numeric work lives in C extensions such as NumPy, which Python coordinates. Dynamic typing does push errors to runtime, surfacing as a TypeError on the line that finally uses the bad value. Type hints checked by mypy move those mistakes back into your editor.
Only the interpreter. Install CPython from python.org, version 3.12 or newer, confirm it with python3 --version, and open a plain text editor; VS Code with the Python extension is the recommendation here. The early chapters use no third-party packages, so there is nothing else to set up until virtual environments and pip arrive.
A dict is a real runtime mapping: keys may be any hashable value rather than only strings, insertion order has been guaranteed since Python 3.7, and a missing key raises KeyError unless you ask with get(). JavaScript objects coerce every key to a string and carry a prototype chain. A TypeScript interface is erased at compile time and describes shape only; TypedDict is Python's checked equivalent.
Reach for a list when items will be added, removed or reordered: it is ordered, mutable and rich with methods. Pick a tuple for fixed records and function returns, because it is immutable and hashable, so it can be a dictionary key. Choose a set for unique values and fast membership checks; it has no order and de-duplicates on creation.
Yes, gradually. Annotate public function signatures first; the interpreter ignores hints at runtime, so a checker such as mypy is what gives them teeth. Add tests the same way: pytest collects any test_*.py file and plain assert statements, and a suite that finishes in a tenth of a second is one you will run after every change. Both habits cost little now and much more later.
JavaScript and TypeScript keep the browser, so anything rendered to the user stays theirs. Python takes the server: Django for full-featured applications, Flask for small ones, FastAPI for typed async APIs where your annotations become automatic documentation. Your TypeScript habits transfer directly, and asyncio mirrors the event loop you already know, just with different keywords.