RizTech Academy logo
RizTech Academy
Modules, Packages and EnvironmentsLesson 1 of 520 min

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:

  1. Built-in modules
  2. The folder containing the script you ran
  3. Folders in the PYTHONPATH environment variable
  4. 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

  1. Create calculations.py with a function and a constant. Import and use both from main.py.
  2. Import the same module four ways. Say when each is appropriate.
  3. Put a top-level print in a module and import it. Confirm it runs.
  4. Add if __name__ == "__main__": with a print, then run the file directly and import it. Print __name__ in both cases.
  5. Import the same module three times in one file. Confirm the top-level code runs once.
  6. Create random.py containing import random and use it. Read the error, then rename the file and delete __pycache__.
  7. Build the utils package with two modules and an __init__.py. Import from it all three ways.
  8. Re-export a function through __init__.py and import it from the package directly.
  9. Create a relative import, run the file directly to see it fail, then run it with -m.
  10. 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