Modules and imports
A module is a Python file. That is the whole definition. Once a program grows past a few hundred lines, splitting it across several files is how it stays manageable — and importing is how those files find each other.
Importing what already exists
You have done this since module 3:
import random
print(random.randint(1, 100))
import random finds a file called random.py in Python's standard library and
makes its contents available under the name random. The dot means "look inside
that module".
Four forms, and the differences matter:
import random
print(random.randint(1, 10))
import random as rnd
print(rnd.randint(1, 10))
from random import randint
print(randint(1, 10))
from random import randint, choice, shuffle
import module keeps everything behind a name. Slightly more typing, and a
reader always knows where random.randint came from. This is the default
choice.
import module as name renames it. Use it only for established conventions —
import pandas as pd, import numpy as np — not to save four characters.
from module import thing brings the name directly into your file. Fine for
a few well-known names. The risk is collisions and lost context: seeing
shuffle(deck) fifty lines later, nobody knows whether that came from random
or from your own code.
Never use from module import *. It pulls in every public name, silently
overwriting anything with a matching name. A NameError or, worse, a wrong
function running with no error at all.
Importing your own files
Two files in the same folder:
# calculations.py
TAX_RATE = 0.18
def apply_tax(amount: float) -> float:
"""Return the amount with tax added."""
return amount + (amount * TAX_RATE)
# main.py
import calculations
print(calculations.apply_tax(100))
print(calculations.TAX_RATE)
No file extension in the import — import calculations, not
import calculations.py. Everything defined at the top level of the file is
available: functions, classes, constants.
Importing runs the file
This is the thing to understand.
# greetings.py
print("greetings.py is running")
def hello():
return "Hello!"
# main.py
import greetings
print(greetings.hello())
greetings.py is running
Hello!
The import executed the whole file, top to bottom. It did not merely make
the functions available — it ran every line, including the print.
Definitions are safe: def creates a function without calling it. Anything else
at the top level happens immediately on import.
So a module that opens a database connection, or prints a banner, or asks for input at the top level does all of that the moment anybody imports it. This is why module-level code should be definitions and constants, and nothing else.
if name == "main"
Module 5 told you to write this and said it would make sense here.
Every module has a __name__. When you run a file directly, Python sets its
__name__ to "__main__". When the file is imported, __name__ is the
module's name instead.
# greetings.py
def hello():
return "Hello!"
print(f"__name__ is {__name__}")
if __name__ == "__main__":
print(hello())
Run it directly:
__name__ is __main__
Hello!
Import it from another file:
__name__ is greetings
The guarded block did not run.
That is the whole mechanism, and it gives you a file that can be both a
program and a library. Module 5's refactored guessing game ends with
main() under that guard — so importing it to reuse describe_guess does not
start a game.
Write it under any code that should only run when the file is executed directly. Test scripts, command-line entry points, demonstrations.
Imports only run once
import calculations
import calculations
import calculations
The file executes once. Python caches imported modules in sys.modules and
subsequent imports reuse the cached one. So repeated imports are free, and a
module's top-level code cannot run twice by accident.
The consequence that surprises people: editing a module will not affect a running REPL session. You changed the file, re-imported, and got the old behaviour. Restart the REPL — reloading is possible but fiddly enough that restarting is almost always correct.
Where Python looks
When you write import something, Python searches, in order:
- Built-in modules
- The folder containing the script you ran
- Folders in the
PYTHONPATHenvironment variable - The installation's site-packages, where pip installs things
import sys
print(sys.path)
Because the script's own folder comes before site-packages, your file can shadow a real module. Module 6 warned about this:
# random.py — a file you created
import random
print(random.randint(1, 10))
AttributeError: module 'random' has no attribute 'randint'
Your file imported itself. The message is baffling until you know the cause.
The names that catch people: random.py, json.py, csv.py, email.py,
string.py, test.py, types.py. Never name a file after something you
import. If you already have, delete the stray .pyc in __pycache__ too.
Packages
A folder of modules is a package:
myproject/
main.py
utils/
__init__.py
formatting.py
validation.py
import utils.formatting
print(utils.formatting.to_rupees(1250))
from utils.formatting import to_rupees
print(to_rupees(1250))
from utils import formatting
print(formatting.to_rupees(1250))
__init__.py marks the folder as a package. It can be empty, and often is.
Python 3.3 and later can treat folders without it as packages, but including it
is clearer and avoids edge cases — write it.
You can also use __init__.py to present a tidy front door:
# utils/__init__.py
from .formatting import to_rupees
from .validation import is_valid_email
Then callers write from utils import to_rupees without knowing which file it
lives in — and you can move it later without breaking anyone.
Relative imports
Inside a package, a leading dot means "relative to here":
# utils/validation.py
from .formatting import to_rupees # same package
from ..config import SETTINGS # one level up
Relative imports only work inside a package, imported as part of it. Running
python utils/validation.py directly gives:
ImportError: attempted relative import with no known parent package
That confuses everybody once. Run it as a module from the project root instead:
python -m utils.validation
-m imports and runs it as part of the package, so the relative import resolves.
Circular imports
Two modules importing each other:
# a.py
import b
# b.py
import a
Python part-executes one, hits the other, and comes back to a module that is not
finished — giving ImportError: cannot import name ... (most likely due to a circular import).
Occasionally fixable by moving an import inside a function, but that treats the symptom. A circular import nearly always means the split between the two files is wrong: either they should be one module, or the shared part belongs in a third that both import.
Practice
- Create
calculations.pywith a function and a constant. Import and use both frommain.py. - Import the same module four ways. Say when each is appropriate.
- Put a top-level
printin a module and import it. Confirm it runs. - Add
if __name__ == "__main__":with a print, then run the file directly and import it. Print__name__in both cases. - Import the same module three times in one file. Confirm the top-level code runs once.
- Create
random.pycontainingimport randomand use it. Read the error, then rename the file and delete__pycache__. - Build the
utilspackage with two modules and an__init__.py. Import from it all three ways. - Re-export a function through
__init__.pyand import it from the package directly. - Create a relative import, run the file directly to see it fail, then run it
with
-m. - Create a circular import deliberately. Read the error, then fix it by moving the shared code into a third module.
Next: the standard library — what Python already gives you before you install anything.
Stuck on this lesson?
Being stuck is part of it — but being stuck alone for three days is not. Our internship programme pairs this curriculum with code review and one-to-one help from working developers, and it is free.
About the internship