Modules and Imports
Our country data and our logic have been living in one file. Real projects split code into modules โ this session is about how Python finds, loads, and shares code across files.
Code split across files, joined back together with import.
1. Learning Objective
By the end of this session you will be able to:
- Split code across multiple .py files and import between them
- Use import module and from module import name, and explain the difference
- Alias an import with as
- Explain what if __name__ == "__main__": guards and why it matters
- Understand why a module is only executed once, no matter how many times it is imported
2. Pre-Coding Quiz
Answer these before reading the concept explanation or writing any code. It is fine to get these wrong โ that is the point. You need 4/5 to proceed to the lab.
You have a file data.py containing countries = [...]. In another file, which import makes countries directly usable by that name (no prefix)?
from data import countries pulls the specific name countries directly into the importing file's namespace. With plain import data, you would need to write data.countries instead.If you use import data instead, how do you then access the countries list defined in data.py?
import data gives you the whole module as an object named data. Everything it defines is accessed through dot notation on that module object: data.countries.What does import numpy as np do?
as creates a local alias for the imported module โ the module itself is unchanged, you've just given yourself a shorter or clearer name to refer to it by in this file.What is the purpose of if __name__ == "__main__": at the bottom of a script?
__name__ to "__main__". When the same file is imported by another module, __name__ is set to the module's name instead. This guard lets a file provide both reusable functions AND a runnable demo, without the demo firing every time it's imported.You import the same module twice from two different files in your project (e.g. both app.py and tests.py import data.py). How many times does data.py's top-level code actually execute?
3. The Concept โ Modules and Imports
import module gives you one namespaced object; from module import name gives you the names directly, unprefixed.
A module is just a .py file
Any Python file can be imported by another. The filename (without .py) becomes the module name.
# data.py
countries = [
{"name": "Kenya", "region": "Africa"},
{"name": "Ghana", "region": "Africa"},
]
def find_by_region(region):
return [c for c in countries if c["region"] == region]
import module vs from module import name
Both forms load the same file. They differ in what name(s) end up available in the importing file.
# main.py โ Option 1: import the whole module
import data
print(data.countries)
print(data.find_by_region("Africa"))
# main.py โ Option 2: import specific names directly
from data import countries, find_by_region
print(countries)
print(find_by_region("Africa"))
Aliasing with as
You can rename what you import, either to shorten a long name or to avoid a clash with something already defined.
import data as d
print(d.countries)
from data import find_by_region as search
print(search("Africa"))
The if __name__ == "__main__": guard
Every Python file has a built-in variable __name__. When the file is run directly, it equals "__main__". When the file is imported by another file, it equals the module's own name instead. This guard lets a file define reusable functions AND include a demo that only runs when the file is executed directly.
# data.py
countries = [{"name": "Kenya", "region": "Africa"}]
def find_by_region(region):
return [c for c in countries if c["region"] == region]
if __name__ == "__main__":
# This block only runs when you execute: python data.py
# It does NOT run when another file does: import data
print(find_by_region("Africa"))
A module's top-level code runs once, and only once
Python caches every module after its first import. If ten different files import the same module, its top-level code still only executes a single time โ every importer shares the same already-built module object.
# counter.py
print("data.py is loading...")
value = 0
# app.py
import counter # prints "data.py is loading..." โ first time
import counter as c2 # does NOT print again โ already cached
print(counter is c2) # True โ same object
4. Lab
What you will build
Two files: country_data.py (the module) and explorer.py (imports and uses it).
Step-by-step instructions
Create country_data.py with the list and one function
# country_data.py
countries = [
{"name": "Kenya", "region": "Africa", "population": 54000000},
{"name": "Ghana", "region": "Africa", "population": 31000000},
{"name": "Peru", "region": "Americas", "population": 33000000},
]
def find_by_region(region):
return [c for c in countries if c["region"] == region]
if __name__ == "__main__":
print("Running country_data.py directly")
print(find_by_region("Africa"))
Run country_data.py directly and observe the guard firing
Confirm the __main__ block runs when you execute the file itself.
Create explorer.py using import module style
# explorer.py
import country_data
print(country_data.countries)
print(country_data.find_by_region("Africa"))
Run explorer.py and confirm the __main__ block did NOT run
You should NOT see "Running country_data.py directly" โ only the two lines you explicitly printed.
Change explorer.py to use from...import with an alias
# explorer.py
from country_data import find_by_region as search
print(search("Africa"))
5. Expected Files Changed
| File | Action | Why |
|---|---|---|
country_data.py |
Created | The reusable module โ data and functions, with a __main__ guard. |
explorer.py |
Created | Imports and uses country_data two different ways. |
docs/sessions/session-10/index.html |
Created | This session document. |
6. Commit Checkpoint
Once the lab is complete and you can explain every line, make this exact commit:
git add country_data.py explorer.py docs/sessions/session-10/index.html
git commit -m "session-10: split data into a module, import it two ways, add a __main__ guard"
7. Code Review Checklist
Go through your code line by line and check each item:
8. Post-Coding Quiz
Same 5 questions. Take it again now that you have written and run the code. You need 4/5 to mark this session complete.
You have a file data.py containing countries = [...]. In another file, which import makes countries directly usable by that name (no prefix)?
from data import countries pulls the specific name countries directly into the importing file's namespace. With plain import data, you would need to write data.countries instead.If you use import data instead, how do you then access the countries list defined in data.py?
import data gives you the whole module as an object named data. Everything it defines is accessed through dot notation on that module object: data.countries.What does import numpy as np do?
as creates a local alias for the imported module โ the module itself is unchanged, you've just given yourself a shorter or clearer name to refer to it by in this file.What is the purpose of if __name__ == "__main__": at the bottom of a script?
__name__ to "__main__". When the same file is imported by another module, __name__ is set to the module's name instead. This guard lets a file provide both reusable functions AND a runnable demo, without the demo firing every time it's imported.You import the same module twice from two different files in your project (e.g. both app.py and tests.py import data.py). How many times does data.py's top-level code actually execute?
9. Reflection Questions
Think through these after the post-quiz. No right answer โ they are for discussion.
- Why would putting a "demo" print statement at the top level of country_data.py (outside the guard) have caused a problem in explorer.py?
- When would you prefer
import moduleoverfrom module import name, given that the second saves typing? - What real-world project structure benefit comes from every module only executing once, even if imported from many places?
- How does a Python module's import caching remind you of anything else you've learned about references and shared state (Session 05)?
10. What Breaks If This Knowledge Is Missing?
- Demo code leaking into imports: Without the
__main__guard, any print statements, test calls, or demo code at the top level of a module will fire every single time that module is imported anywhere โ cluttering output and sometimes causing real side effects in production code. - Circular imports: Once code is split across files, it becomes possible for module A to import module B while B tries to import A โ a circular import error. Understanding how imports execute (once, top to bottom) is the first step to debugging this later.
- Project architecture (Layer 6): Sessions 36โ40 are entirely about organizing a growing codebase into modules and packages. Everything there assumes you are comfortable splitting files and importing between them.
11. What We Learned
Python concept mastered: Modules and the import system โ import vs from-import, aliasing, and the __main__ guard.
Unlocks: You can now split growing code across multiple files instead of one giant script โ the basis for any real Python project structure.
Next session: Session 11 โ Errors and Exceptions. We will learn to handle things going wrong โ try/except and raising exceptions โ before we build anything more complex.