Modules & Imports
A notebook is fine for learning. The moment you build something real, your code lives in several files and you need to know how they find each other.
This page is short, and it prevents a category of confusion that wastes whole evenings.
A module is a file
Any .py file is a module. Its name is the filename without the extension.
tools.py -> module "tools"
agent.py -> module "agent"To use one from another, import it.
import tools
result = tools.get_weather("Hyderabad")Three ways to import
import json
json.dumps(data)
from typing import Optional, Annotated
value: Optional[str] = None
import pandas as pd
df = pd.DataFrame(rows)import x— brings in the module; you usex.thingfrom x import thing— brings in one name directlyimport x as y— brings it in under a shorter name
All three are normal. Use the second when you want two or three specific names, the first when you want the module's identity visible in the code.
The one to avoid
from tools import *This imports everything and hides where each name came from. When two modules both define search, you get whichever was imported last and no warning. Every Python style guide advises against it, and rightly.
A package is a folder
myagent/
__init__.py
tools.py
prompts.py
graph.py
main.pyThe __init__.py file marks the folder as a package. It is very often empty, and that is fine — its presence is the point.
from myagent.tools import get_weather
from myagent import promptsThe import path, and the error you will actually hit
ModuleNotFoundError is the most common import problem, and it has two quite different causes.
Cause one: the package is not installed.
ModuleNotFoundError: No module named 'langchain'Fix: pip install langchain.
Cause two: it is installed, but not for the Python you are running.
This is the one that wastes evenings. You have two Pythons on the machine — a system one and a virtual environment, say — you installed into one and are running the other.
Check which Python is actually running your code:
import sys
print(sys.executable)Then install into that exact one:
python -m pip install langchainUsing python -m pip rather than a bare pip guarantees the install goes to the interpreter you are running. It is a good habit and it removes the whole class of problem.
Virtual environments
A virtual environment is a private folder of packages belonging to one project. Without one, everything installs system-wide and two projects needing different versions of the same library will fight.
python -m venv .venvActivate it:
.venv\Scripts\activateOn Mac or Linux:
source .venv/bin/activateYour prompt gains a (.venv) prefix. Now pip install goes into that folder and nowhere else.
Record what you installed:
pip freeze > requirements.txtAnd on another machine:
pip install -r requirements.txtOne environment per project. Add .venv/ to your .gitignore.
The __main__ line
def run():
print("running the agent")
if __name__ == "__main__":
run()Every module has a __name__. When you run a file directly it is "__main__"; when the file is imported it is the module's name.
So that block means: run this only when the file is executed directly, not when something imports it. It is Python's public static void main, and it is why importing a module does not accidentally start it.
Without it, import tools would run everything in tools.py, including the demo code at the bottom.
Circular imports
If agent.py imports tools.py and tools.py imports agent.py, you get:
ImportError: cannot import name 'x' from partially initialized moduleThe message is confusing but the cause is simple: two files each waiting for the other to finish loading.
The fix is almost never technical — it is that the two files are entangled. Pull the shared thing into a third module both can import. In agent projects this is usually configuration, or the model client, or shared types.
A layout that works
For a small agent project:
myagent/
__init__.py
config.py settings and keys, read from the environment
tools.py your tool functions
prompts.py prompt text, kept out of the logic
graph.py the agent or graph itself
tests/
test_tools.py
main.py the entry point
requirements.txt
.env secrets, never committed
.gitignoreNothing clever, and it scales further than you would expect. The one rule worth keeping from day one: prompts and keys do not live inside logic files. You will change prompts constantly, and you will want to change them without touching code.
Coming from Java or C#
| Concept | Java | C# | Python |
|---|---|---|---|
| File of code | .java, one public class | .cs | .py module, any contents |
| Grouping | package com.acme | namespace Acme | a folder with __init__.py |
| Import | import com.acme.Tool; | using Acme; | from acme import tool |
| Dependencies | Maven, Gradle | NuGet | pip |
| Manifest | pom.xml | .csproj | requirements.txt |
| Isolation | per-project by default | per-project by default | you must create a venv |
| Entry point | public static void main | Main | if __name__ == "__main__": |
The row that catches people: Java and C# isolate dependencies per project automatically. Python does not. If you skip the virtual environment, everything lands in one shared pile.
Also note a file is not tied to one class. A single .py can hold several classes and functions, and usually should.
Common mistakes
- A bare
pip installinto the wrong interpreter. Usepython -m pip install. - Skipping the virtual environment and then wondering why upgrading one project broke another.
from x import *, which hides the origin of names.- Committing
.venv/to git. Add it to.gitignore. - Naming a file after a library — a
json.pyin your folder will shadow the realjsonand produce baffling errors.
Practise this
- Create a virtual environment, activate it, install one package, and run
pip freeze. - Print
sys.executableinside and outside the environment and compare. - Split a working script into
tools.pyandmain.py, and import one from the other. - Add an
if __name__ == "__main__":block and confirm it does not run when imported.