Section · 01
A module is a file
Anything you save as a .py file is a module. The file utils.py defines a module named utils. To use its contents from another file, import it.
# utils.py
def slugify(text):
return text.lower().replace(" ", "-")
def truncate(text, length=80):
if len(text) <= length:
return text
return text[:length - 1] + "…"
TITLE_PREFIX = "YorkSims · "# main.py — in the same folder
import utils
print(utils.slugify("Hello World")) # "hello-world"
print(utils.truncate("a very long...", 5)) # "a ve…"
print(utils.TITLE_PREFIX) # "YorkSims · "That’s the entire mechanism. Functions, classes, variables defined at the top of a module are accessible from outside once someone imports the module.
Section · 02
The three import forms
Same module, three syntaxes:
1. import module
import utils
utils.slugify("Hello World")Brings the whole module in under its own name. You access things as utils.something. Safest default — no name collisions, and a reader can see exactly where each name came from.
2. from module import name
from utils import slugify
slugify("Hello World") # no prefix neededPulls a specific name into the current namespace. Shorter at the call site, but you lose the “where did this come from” context. Use when you call something often and the source is obvious from context.
3. import module as alias
import numpy as np
import pandas as pd
arr = np.array([1, 2, 3])
df = pd.DataFrame(...)Useful for long module names. The community has standard aliases for big libraries (np for numpy, pd for pandas, pltfor matplotlib). Use the conventional alias when one exists; don’t invent new ones for your own modules unless they’re truly long.
The fourth form you should NOT use
from utils import * # DON'TStar-imports dump every name from the module into your namespace. Three problems with that:
1. If utils.py adds a new function tomorrow that happens to share a name
with one of YOUR variables, the import silently overwrites yours.
2. A reader looking at your file can't tell where any name came from.
3. Linters can't catch unused imports, since they can't see what was imported.The only place you’ll legitimately see star-imports is in__init__.pyfiles re-exporting a package’s public API. Even there, modern style avoids it.
Section · 03
What the standard library gives you
Python ships with a huge collection of modules — the standard library— for free. You don’t install anything; just import. A handful you’ll meet first:
import math # sqrt, pi, log, sin, ...
import random # randint, choice, shuffle, ...
import datetime # date, datetime, timedelta
import json # dumps, loads — turn data into JSON and back
import os # filesystem and environment
import sys # the interpreter itself (argv, exit, ...)
import re # regular expressions
import pathlib # modern path manipulation
import collections # Counter, defaultdict, deque, OrderedDict
import itertools # chain, combinations, cycle, ...Before you write something from scratch, check the standard library. Want to count occurrences of items in a list? collections.Counter already does it. Want to parse a date string? datetime.strptimealready does it. Knowing what’s there is half the skill of writing Python.
Small example
import json
from datetime import datetime
payload = {
"event": "checkout",
"amount_cents": 4995,
"occurred_at": datetime.now().isoformat(),
}
print(json.dumps(payload, indent=2))Section · 04
Third-party packages — pip and virtualenv
Everything beyond the standard library lives on PyPI (Python Package Index) and is installed with pip:
pip install requestsThen in your code:
import requests
response = requests.get("https://api.yorksims.com/health")
print(response.status_code)
print(response.json())One critical habit: use a virtual environmentper project. A virtual environment is a sandboxed Python install with its own packages, so installing one project’s dependencies doesn’t break another’s.
# Create a virtualenv in the current folder
python -m venv .venv
# Activate it (Mac / Linux)
source .venv/bin/activate
# Activate it (Windows)
.venv\Scripts\activate
# Now pip installs only into this project
pip install requestsModern alternatives — uv, poetry, pipenv— handle this for you. Pick one and stick with it. The one thing you must not do is install third-party packages into the system Python; that’s how you bork your OS.
Section · 05
When (and how) to split your code into modules
A single main.py is fine until it hits ~300 lines and you start having a hard time finding things. The split is driven by purpose, not size:
my_project/
├── main.py # entry point: glues everything together
├── models.py # the data classes
├── services.py # business logic — what your program DOES
├── storage.py # database / file persistence
├── notifications.py # email / SMS / whatever
└── utils.py # small, generic helpers used everywhereThe rule of thumb: each module should have one job that you can describe in a sentence. If you can’t — “models.py has the data classes AND the database access AND a date helper” — split it. Coherent modules are easier to read, easier to test in isolation, and easier to throw away when the requirements change.
The if __name__ == "__main__" guard
# greet.py
def greet(name):
print(f"Hi, {name}.")
if __name__ == "__main__":
greet("Ada") # only runs when you do: python greet.py
# NOT when another file imports this moduleThis pattern lets a file be both an importable module AND a runnable script. The code under the guard runs only when the file is executed directly, not when it’s imported. Use it to put quick test calls or demos at the bottom of a module without polluting other code that imports it.