Functions & Classes

Modules & Imports

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 use x.thing
  • from x import thing — brings in one name directly
  • import 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.py

The __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 prompts

The 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 langchain

Using 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 .venv

Activate it:

.venv\Scripts\activate

On Mac or Linux:

source .venv/bin/activate

Your prompt gains a (.venv) prefix. Now pip install goes into that folder and nowhere else.

Record what you installed:

pip freeze > requirements.txt

And on another machine:

pip install -r requirements.txt

One 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 module

The 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
.gitignore

Nothing 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#

ConceptJavaC#Python
File of code.java, one public class.cs.py module, any contents
Groupingpackage com.acmenamespace Acmea folder with __init__.py
Importimport com.acme.Tool;using Acme;from acme import tool
DependenciesMaven, GradleNuGetpip
Manifestpom.xml.csprojrequirements.txt
Isolationper-project by defaultper-project by defaultyou must create a venv
Entry pointpublic static void mainMainif __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 install into the wrong interpreter. Use python -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.py in your folder will shadow the real json and produce baffling errors.

Practise this

  • Create a virtual environment, activate it, install one package, and run pip freeze.
  • Print sys.executable inside and outside the environment and compare.
  • Split a working script into tools.py and main.py, and import one from the other.
  • Add an if __name__ == "__main__": block and confirm it does not run when imported.

Try It Yourself

Copy this into a new Colab notebook and run it. Nothing to install.

example.py
1# Modules, imports, and the one error that wastes evenings.
2
3import sys
4import json # whole module -> json.dumps
5from pathlib import Path # one name -> Path
6import datetime as dt # renamed -> dt.date
7
8print(json.dumps({"ok": True}))
9print(Path("."). resolve().name)
10print(dt.date(2026, 9, 2))
11print()
12
13# --- WHICH Python am I actually running? ---
14# This is the answer to "I installed it but it says ModuleNotFoundError".
15print("interpreter:", sys.executable)
16print("version :", sys.version.split()[0])
17print()
18
19# Install into THAT interpreter, not whatever `pip` happens to be:
20# python -m pip install httpx
21print("install with: python -m pip install <package>")
22print()
23
24# --- Where Python looks for modules ---
25print("first three search paths:")
26for p in sys.path[:3]:
27 print(" ", p or "(current directory)")
28print()
29
30# --- ModuleNotFoundError, on purpose ---
31try:
32 import a_package_that_does_not_exist
33except ModuleNotFoundError as e:
34 print("ModuleNotFoundError:", e)
35 print(" -> either it is not installed, or it is installed for a DIFFERENT python")
36print()
37
38# --- Building a small package at runtime, to see imports work ---
39Path("myagent").mkdir(exist_ok=True)
40Path("myagent/__init__.py").write_text("", encoding="utf-8") # marks it a package
41Path("myagent/tools.py").write_text(
42 'def get_weather(city):\n'
43 ' """Get the weather for a city."""\n'
44 ' return f"28 degrees in {city}"\n'
45 '\n'
46 'print("tools.py was imported")\n'
47 '\n'
48 'if __name__ == "__main__":\n'
49 ' print("this only runs when tools.py is run DIRECTLY")\n',
50 encoding="utf-8",
51)
52
53from myagent.tools import get_weather
54print(get_weather("Hyderabad"))
55print()
56
57# Notice: "tools.py was imported" printed, but the __main__ block did NOT.
58# That is what `if __name__ == "__main__":` is for - Python's
59# `public static void main`.
60
61print("__name__ in this file is:", __name__)
62
63# --- What to avoid ---
64# from myagent.tools import * <- hides where every name came from
65
66# --- A layout that works ---
67print("""
68myagent/
69 __init__.py
70 config.py settings and keys, read from the environment
71 tools.py your tool functions
72 prompts.py prompt text, kept out of the logic
73 graph.py the agent itself
74main.py
75requirements.txt
76.env never committed
77.gitignore contains .env and .venv/
78""")