AI Computer Institute
Expert-curated CS & AI curriculum aligned to CBSE standards. A bharath.ai initiative. About Us

Python Modules and Packages

📚 Python Mastery⏱️ 22 min read🎓 Grade 8
✍️ AI Computer Institute Editorial Team Updated: August 2026 CBSE-aligned · Peer-reviewed · 22 min read
Content curated by subject matter experts with IIT/NIT backgrounds. All chapters are fact-checked against official CBSE/NCERT syllabi.

Suppose you wrote a function last month for a school weather-logging project — something small, like converting a temperature from Celsius to Fahrenheit. It worked fine. This month you start a completely different project: a fitness tracker that logs your morning run times, and somewhere in it you need that exact same Celsius-to-Fahrenheit conversion again, because the tracker also shows the weather for your run. Do you retype the function? Copy-paste it from the old project? And if you find a bug in it three months from now — a missing bracket, a wrong formula — do you remember to fix it in every single file where you pasted it?

This is the exact problem Python modules exist to solve: writing code once, and reusing it anywhere, without duplication. It sounds like a small convenience, but it is one of the most important ideas in all of programming — every serious Python program, from a 20-line school project to the software running a satellite, is built by combining smaller, reusable pieces rather than one giant file. This chapter is about how Python lets you do that: modules, the import system, and packages.

What exactly is a module?

Strip away the terminology for a second. A Python module is nothing more than a file ending in .py that contains Python code — variables, functions, maybe classes later on. That's it. Every .py file you have ever written is already a module, whether you knew it or not. The word "module" only becomes meaningful once you start using code from one file inside another file. That act of pulling code from one file into another is called importing, and the keyword that does it is import.

Python actually ships with a huge collection of ready-made modules, called the standard library, so you don't have to write everything from scratch. One of them is math, which contains functions and constants for mathematical work. Let's trace exactly what happens when you use it.

import math

print(math.sqrt(49))
print(math.pi)
print(math.floor(7.9))

Read this line by line, the way Python actually executes it:

  • import math — Python finds the file math (it is actually built into the interpreter itself, written in C for speed, but Python treats it exactly like any other module) and runs it, making everything inside it available under the name math.
  • math.sqrt(49) — calls the sqrt function that lives inside the math module. Since 49 is a perfect square, this returns 7.0 (note: a float, not the integer 7 — square root can produce decimals, so Python always returns a float here).
  • math.pi — this is not a function, just a stored value (a constant) inside the module. It gives 3.141592653589793.
  • math.floor(7.9) — rounds down to the nearest whole number, giving 7.

The output, in order, is:

7.0
3.141592653589793
7

Notice the dot: math.sqrt, not just sqrt. This dot is not decoration — it's Python telling you exactly where sqrt came from. This matters more than it seems, and we'll see exactly why in a few minutes.

The four ways to import

Python gives you a few different phrasings for import, and each one changes what name you type afterward to use the code.

1. Plain importimport math — you must always write math.something. This is the safest and clearest style, because anyone reading your code instantly knows sqrt came from the math module.

2. Import with an alias — sometimes a module's name is long and you'll type it hundreds of times, so Python lets you rename it on the way in:

import math as m
print(m.sqrt(64))

Output: 8.0. This is exactly the same module, just referred to by a shorter nickname, m, for the rest of the file.

3. Importing a specific name — if you only need one function, you can pull just that name out of the module directly into your file, dropping the dot:

from math import sqrt
print(sqrt(81))

Output: 9.0. Notice you now write sqrt(81), not math.sqrt(81) — the name sqrt has been copied directly into your file's own namespace.

4. Importing everything with a starfrom math import * pulls every name from the module directly in, so you could write sqrt(81), pi, and floor(7.9) all without the math. prefix. This looks convenient. Later in this chapter you will see exactly why experienced programmers avoid it — it is one of the most common sources of subtle, hard-to-find bugs for beginners.

Building your own module

Standard-library modules are useful, but the real power of this idea is writing your own reusable modules. Suppose you create a file named circles.py with this content:

# circles.py
PI = 3.14159

def area(radius):
    return PI * radius * radius

def circumference(radius):
    return 2 * PI * radius

Now, in a different file, saved in the same folder, named main.py, you write:

# main.py
import circles

r = 7
print("Area:", circles.area(r))
print("Circumference:", circles.circumference(r))

Trace it: import circles loads your file circles.py exactly the same way it loaded the built-in math module — Python does not distinguish between "modules you wrote" and "modules that came with Python." Then circles.area(7) computes 3.14159 × 7 × 7 = 153.93791, and circles.circumference(7) computes 2 × 3.14159 × 7 = 43.98226. The output is:

Area: 153.93791
Circumference: 43.98226

Two details that trip up beginners here. First, when you write import circles, you never include the .py extension — Python adds that automatically when it searches for the file. Writing import circles.py is a syntax error. Second, main.py and circles.py must sit in the same folder (or Python must otherwise be told where to look), because by default Python only searches the current folder and a few standard locations for modules to import — it does not search your entire computer.

What actually happens when a module is imported

Here is a question worth sitting with: when Python "imports" a file, does it just copy the function definitions over, like a dictionary lookup? Or does it genuinely run the file, top to bottom, like a script?

The answer, and this surprises most beginners, is the second one. Importing a module runs every line of that file, from top to bottom, exactly once — not just the function definitions, but any plain statement sitting outside a function too. Watch this file, greet.py:

# greet.py
def hello():
    print("Hello from greet module!")

print("This runs when greet.py is loaded:", __name__)

if __name__ == "__main__":
    hello()

There's a new piece here: __name__. Every module, while it is running, has a hidden variable called __name__ that Python fills in automatically. The rule is simple: if the file is the one you directly ran (typed python greet.py at the terminal), Python sets __name__ to the string "__main__". If the file was instead imported by some other file, Python sets __name__ to the module's own name — here, "greet".

So running greet.py directly gives:

This runs when greet.py is loaded: __main__
Hello from greet module!

because the if __name__ == "__main__": check is true, so hello() gets called too.

But if another file does import greet, the output is only:

This runs when greet.py is loaded: greet

The print statement outside the function still runs — remember, importing runs the whole file — but hello() is never called, because __name__ is now "greet", not "__main__", so the if condition is false.

This is exactly why almost every Python file you'll write from now on that is meant to be reusable ends with if __name__ == "__main__": — it lets a file behave as a standalone program when run directly, and as a quiet, well-behaved library of functions when imported elsewhere, with no unwanted printing or side effects sneaking out.

Misconception: "import re-runs the file every time"

A very natural but incorrect assumption is that writing import somemodule a second time re-executes that module's code again. It does not. Python is smart about this — the first time a module is imported anywhere in a program, Python runs it once and stores the finished result in memory (in a lookup table called sys.modules). Every subsequent import of that same module, anywhere else in the program, just reuses that already-built result instead of running the file again.

Watch this play out. Save this as counter.py:

# counter.py
print("counter.py is being loaded...")
count = 0

def increment():
    global count
    count += 1
    return count

And this as main.py in the same folder:

import counter
import counter

print(counter.increment())
print(counter.increment())

If importing genuinely re-ran the file each time, you'd expect to see "counter.py is being loaded..." printed twice, once for each import counter line. But the actual output is:

counter.py is being loaded...
1
2

The loading message appears only once. The second import counter did essentially nothing — Python checked, saw counter was already loaded, and skipped straight past it. This is also why the count variable keeps its value across the two increment() calls (going from 0 to 1, then 1 to 2): there's only ever one copy of counter.py's data in memory for the whole program, no matter how many files import it.

Misconception: from module import * is a harmless shortcut

This is the single most common beginner trap with imports, and it deserves a full worked example rather than just a warning. Save this as shapes.py:

# shapes.py
def area(radius):
    return 3.14159 * radius * radius

And this as geometry.py:

# geometry.py
def area(side):
    return side * side

Used carefully, with plain imports, there's no problem at all — each module keeps its own name attached to its function, so both coexist peacefully:

import shapes
import geometry

print(shapes.area(5))
print(geometry.area(5))

Trace it: shapes.area(5) treats 5 as a circle's radius, giving 3.14159 × 5 × 5 = 78.53975. geometry.area(5) treats 5 as a square's side, giving 5 × 5 = 25. Output:

78.53975
25

Two completely different area functions, both usable in the same program, with zero confusion — because the module name in front of the dot keeps them apart.

Now watch what happens with star imports:

from shapes import *
from geometry import *

print(area(5))

Here's the trap. from shapes import * copies the name area (the circle-area version) directly into your file. Then from geometry import * copies its own name area (the square-area version) directly into your file too — and since a variable name can only point to one thing at a time, this second import silently overwrites the first one. There is no error, no warning. When you finally call area(5), you get:

25

Not 78.53975. The circle-area function is gone, quietly replaced, and nothing in the program tells you this happened. In a small two-module example like this, you might catch the bug quickly. In a real program built from a dozen modules, a silently overwritten function can waste hours of confused debugging. This is precisely why the plain import module_name style, with the dot, is the one professional Python code almost always uses — the dot is not extra typing, it is a safety feature.

From modules to packages

A single module file is fine for a handful of related functions. But a real project — say, a set of tools for running your school's annual sports-day scoring system — quickly grows to need many separate files: one for cricket scoring, one for athletics timing, one for generating certificates, and so on. Dumping fifty unrelated .py files into one folder becomes as messy as dumping fifty loose papers into one school bag instead of organizing them into labelled notebooks. Python's answer to this is the package.

A package is simply a folder containing related modules, plus one special file inside it named __init__.py that marks the folder as "this is a package, not just any folder" (in modern Python, this file is technically optional, but including it — even empty — is standard practice and required knowledge for your exams, since it also lets you control exactly what the package exposes when imported). Packages can even contain other packages inside them, called sub-packages, letting you build a whole tree of organized code.

Here's a package for that sports-day project:

school_pkg/ __init__.py calculator.py converter.py games/ __init__.py quiz.py package / sub-package (a folder) module (a .py file with code) __init__.py (marks the folder as a package)

With this structure on disk, code in a file outside school_pkg can reach into it using dotted paths that mirror the folder structure exactly:

from school_pkg import calculator
from school_pkg.games import quiz

print(calculator.add(3, 4))

Read the dots as forward slashes in a file path: school_pkg.games means "the games folder inside the school_pkg folder" — exactly the same relationship you'd see if you opened your computer's file explorer and clicked into school_pkg, then into games. If calculator.py contains a function add(a, b): return a + b, then calculator.add(3, 4) outputs 7. Packages don't introduce any new importing rules — they simply extend the same dotted-name idea from "module.function" to "package.module.function", one folder level at a time.

The standard library versus third-party packages

It's worth being precise about a distinction CBSE exams often test: Python code you can import falls into two categories. The standard library is the set of modules and packages that come installed with Python itself — no downloading required. You've already met math; others you'll use often include random (for generating random numbers, useful for simulations or games), datetime (for working with calendar dates and times), statistics (mean, median, and similar calculations on lists of numbers), and os (for interacting with files and folders on your computer).

The second category is third-party packages — code written by other programmers around the world, published to a central online catalogue called the Python Package Index, or PyPI, and not included with Python by default. To use one, you first install it onto your computer using a tool called pip (Python's package installer), typically by typing a command like pip install numpy at your terminal — outside of any Python file, as a one-time setup step — after which you can import numpy inside your programs exactly like any standard-library module. Packages like numpy (fast numerical arrays and calculations) and pandas (organizing and analyzing tabular data, the kind you'd see in a spreadsheet) are widely used across Indian engineering colleges and data-analysis roles precisely because they save you from re-implementing well-tested mathematical code yourself — the same reuse principle from the start of this chapter, just at a much larger scale, built by thousands of contributors instead of one student.

Peeking inside a module: dir() and help()

Once you start importing modules you didn't write yourself, a natural question is: what's actually inside this thing? Python gives you two built-in tools to look. dir(math) returns a list of every name defined inside the math module — function names like 'sqrt', 'floor', 'sin', 'cos', and constants like 'pi' and 'e' are all in that list, alongside some special names Python adds automatically that you can ignore for now. And help(math.sqrt) prints a short description of exactly what that one function does, what it expects as input, and what it returns — genuinely useful when you forget a function's exact behavior mid-exam-prep or mid-project, and far faster than searching for it online.

Quick recap of the misconceptions this chapter corrected

  • Importing a module a second time does not re-run its code — Python caches the result after the first import and reuses it every time after.
  • A module's top-level code (anything not inside a function) runs completely when the module is imported — the if __name__ == "__main__": guard is what stops certain code from running on import, not the act of defining functions.
  • from module import * can silently overwrite names you already imported from a different module, with no error message — writing module.name with the dot avoids this entirely.
  • You never include the .py extension in an import statement — Python adds it automatically while searching for the file.

Check your understanding

  1. You have a file tools.py containing def double(x): return x * 2. Write the two lines needed in a separate file to import just this function (not the whole module) and call it on the number 9. What does it print?
  2. A file setup.py starts with the line print("loading setup module") written outside any function. If three different files in your project each write import setup, how many times does "loading setup module" actually get printed in total, and why?
  3. You write from alpha import * followed by from beta import *, and both modules happen to define a function called process(). Which module's process() will actually run if you call process() next — and what should you have written instead to avoid this ambiguity altogether?
  4. Inside a package folder named tools_pkg, there is a sub-folder text containing a module cleaner.py with a function strip_spaces(). Write the import statement, using the full dotted path, that lets an outside file call cleaner.strip_spaces().
  5. Explain, in your own words, the actual difference between import math and from math import sqrt — not just how they're typed, but what each one does to your program's available names.

Self-check answers: (1) from tools import double then print(double(9)), printing 18. (2) Once — the first import setup encountered anywhere in the running program executes the file and caches it; the other two imports find it already in Python's cache and skip re-running it. (3) beta's process() runs, because it was imported second and silently overwrote alpha's version under the same name; writing import alpha and import beta instead, then calling alpha.process() or beta.process() explicitly, avoids the ambiguity. (4) from tools_pkg.text import cleaner (then call cleaner.strip_spaces()), or equivalently from tools_pkg.text.cleaner import strip_spaces to call it directly as strip_spaces(). (5) import math only makes the single name math available, and every function inside it must be reached through that name with a dot, like math.sqrt; from math import sqrt instead copies just the name sqrt directly into your file's own namespace, letting you write sqrt(x) alone, but it does not make any other name from the math module — like pi or floor — available at all.

Summary

A module is any .py file, and importing one runs that file's code once and gives you access to whatever it defines, either through a dotted name (import module, the safer default) or copied directly into your own file (from module import name, useful sparingly, and risky when used with a star). Python's own standard library — math, random, datetime, and dozens more — ships ready to import, while third-party packages from PyPI, installed with pip, extend this same system to code written by the wider world. When a project outgrows a single module, a package — a folder with an __init__.py — lets you organize many related modules, and even sub-packages, under one dotted-name tree that mirrors your actual folder structure. Underneath all of this sits one simple, powerful idea you'll rely on for the rest of your programming life: write it once, name it clearly, and reuse it everywhere.

Think About It

Think about this: How would you explain python modules and packages to a friend who has never seen a computer? What real-world analogy would you use? Imagine you had to build a system using these concepts — what would be your first step? Try this: before moving on, write down three things you learned and one question you still have.

← APIs with Python: Fetching Web DataTesting and Debugging Python Code →

Found this useful? Share it!

📱 WhatsApp 🐦 Twitter 💼 LinkedIn