The problem: the same function, copied five times
Imagine you are working on Computer Science projects across the school year. In August, you write a function that calculates a student's percentage from marks. In October, you need the exact same calculation for a different project — a report-card generator. You do not import anything; you simply select the function, copy it, and paste it into the new file. In December, a classmate points out that your percentage formula has a rounding bug. You fix it in the report-card generator. But the original August file still has the bug. So does the January file where you copied it a third time. So does the file you emailed to a friend for a group project.
This is not a hypothetical annoyance — it is the single biggest reason real software breaks in ways that are hard to trace. The same logic exists in several places, and fixing it in one place does not fix it everywhere. Python's answer to this problem is the module: write the function once, save it in its own file, and then reuse that file from anywhere else, instead of retyping it. When a set of related files grows large enough to organize into folders, Python calls that organized folder a package. Together, modules and packages are how every piece of reusable Python code — from a five-line helper you wrote yesterday to a library used by millions of programmers — is structured. This chapter builds that idea from the ground up, using code you can run exactly as written.
What a module actually is
You have almost certainly already used a module without thinking of it that way. Consider this program:
import math
radius = 7
area = math.pi * radius ** 2
print("Area of circle:", area)
Trace it carefully. import math loads a file called math that ships with every Python installation, and makes its contents available under the name math. Inside that file is a variable named pi, holding the value 3.141592653589793, and dozens of functions. The line math.pi * radius ** 2 first computes radius ** 2, which is 7 ** 2 = 49, then multiplies by math.pi. The result is approximately 153.94, and that is what gets printed after the label.
Here is the definition you should hold onto: a module is nothing more than a single .py file. The file named math that Python ships with is a module in exactly the same sense that a file you write yourself is a module. There is no special "module format" to learn — you already know how to write one, because you already know how to write a .py file. The word "module" simply describes the role a file plays when another file imports it, rather than runs it directly.
Writing and importing your own module
Let's build one. Save this as geometry.py:
def area_of_circle(radius):
"""Return the area of a circle with the given radius."""
return 3.14159 * radius ** 2
def area_of_rectangle(length, breadth):
"""Return the area of a rectangle."""
return length * breadth
Notice that this file, on its own, does nothing when you run it — it only defines two functions and stops. Now, in the same folder, save a second file, main.py:
import geometry
print(geometry.area_of_circle(7))
print(geometry.area_of_rectangle(4, 5))
When you run main.py, here is exactly what happens, step by step. First, Python sees import geometry and searches the current folder for a file named geometry.py. It finds it, and executes every top-level line inside that file from top to bottom — in this case, that means defining the two functions and nothing else, since there is no other top-level code. Python then binds the name geometry in main.py to everything that file produced. The next line, geometry.area_of_circle(7), reaches inside that bundle, finds area_of_circle, and calls it with radius = 7. Inside the function, 3.14159 * 7 ** 2 = 3.14159 * 49 = 153.93791, so the first line printed is 153.93791. The second call, area_of_rectangle(4, 5), returns 4 * 5 = 20, so the second printed line is 20.
The dot in geometry.area_of_circle is doing real work, not just decoration. It tells Python: "look inside the geometry module for a name called area_of_circle," rather than searching the whole program for any function with that name. This is called a namespace — every module keeps its own private collection of names, and you reach into it explicitly with the dot. Namespaces are the entire reason modules solve the copy-paste problem safely, as the next section shows.
Why the dot matters: two files, one function name, zero conflict
Suppose you are building a small billing tool for an online shopping project. One file handles festive-sale discounts, another handles the shopping cart total. Both, quite naturally, need a function called total — but they mean different things by it. Save discount_calculator.py:
def total(price, discount_percent):
return price - (price * discount_percent / 100)
and billing.py:
def total(item_prices):
return sum(item_prices)
Now use both from main.py:
import discount_calculator
import billing
print(discount_calculator.total(2000, 10))
print(billing.total([250, 499, 1200]))
Trace this. discount_calculator.total(2000, 10) computes 2000 * 10 / 100 = 200.0, then 2000 - 200.0 = 1800.0 — a price of ₹2000 after a 10% festive discount. billing.total([250, 499, 1200]) adds the list to get 1949 — a cart total. Both functions are named total, both are imported into the same program, and there is no conflict whatsoever, because each lives in its own module's namespace. You reach the discount one through discount_calculator.total and the cart one through billing.total. This is precisely what breaks when programmers reach for a shortcut that looks convenient but is not: from module import *.
Common misconception — "from module import * is just a shortcut with no real downside." It is a shortcut, but it removes the very protection that namespaces give you. Watch what happens if you rewrite the imports:
from discount_calculator import *
from billing import *
print(total(2000, 10))
Trace it precisely. The first line copies the name total (the discount version, which takes two arguments) directly into main.py's own namespace — no dot needed anymore. The second line then copies billing's total (which takes one argument, a list) into that same namespace, and because both are called total, the second one silently overwrites the first. By the time print(total(2000, 10)) runs, total refers to billing.total, which only accepts one argument. Python raises TypeError: total() takes 1 positional argument but 2 were given. The bug is not a typo — every line is syntactically correct Python. The bug is that star-imports silently discard the namespace protection that dotted imports give you for free. This is why professional Python style (and CBSE-level good practice) favours import module_name or, at most, naming exactly the functions you need with from module import specific_function.
Three import styles, and when to use each
import geometry— safest and most explicit. You always access names asgeometry.something, so there is never any ambiguity about where a name came from.import geometry as geo— identical behaviour, shorter name. Useful when the module name is long or you import it in many lines. For example,geo.area_of_rectangle(6, 3)returns18, exactly asgeometry.area_of_rectangle(6, 3)would.from geometry import area_of_circle— pulls exactly one named function into your file directly, so you writearea_of_circle(10)without the prefix. This is safe as long as you know that name is not already used elsewhere in your file — you are trading a little namespace safety for shorter code, deliberately and by name, rather than blindly importing everything with*.
Trace the third one to be sure it is understood: from geometry import area_of_circle followed by print(area_of_circle(10)) computes 3.14159 * 10 ** 2 = 3.14159 * 100 = 314.159.
Does importing re-run the file every time?
Common misconception — "every time I call a function from an imported module, Python re-reads and re-runs that whole file." It does not. Python runs a module's top-level code once, the first time it is imported anywhere in a running program, and then keeps the finished result cached in memory. Every later import of that same module — even from a different file, later in the same program — reuses the cached version instantly, without re-executing anything. You can observe this directly. Add a top-level print statement to geometry.py, outside any function:
print("geometry.py is loading...")
def area_of_circle(radius):
return 3.14159 * radius ** 2
Now import it twice from two different places in one program — say, once in main.py and once inside another module that main.py also imports. "geometry.py is loading..." prints exactly once, the first time it is needed, no matter how many separate import geometry statements exist across your program. This caching is also why importing a module is fast even when it contains a great deal of code: after the first load, later imports are just a lookup, not a re-run.
A module that can also run itself: __name__ == "__main__"
Common misconception — "a module and a runnable script are two different kinds of file." They are not. The exact same .py file can be run directly (python geometry.py) or imported by another file (import geometry) — Python does not require you to choose. What changes between the two situations is a special built-in variable called __name__. When a file is executed directly, Python sets that file's __name__ to the string "__main__". When the same file is instead loaded via import, Python sets its __name__ to the module's own name — here, "geometry". This lets one file behave differently depending on how it was started:
def area_of_circle(radius):
return 3.14159 * radius ** 2
if __name__ == "__main__":
print("Running geometry.py directly as a test")
print(area_of_circle(5))
If you run this file directly with python geometry.py, __name__ equals "__main__", the condition is True, and it prints the test line followed by 3.14159 * 5 ** 2 = 78.53975. But if another file does import geometry, __name__ inside geometry.py is set to "geometry" instead, the condition is False, and none of that test code runs — only the function definition happens. This idiom is extremely common in real Python code: it lets a file serve two purposes at once — a reusable module when imported, and a self-test or demo when run on its own — and CBSE Computer Science papers do ask students to trace the printed output of exactly this pattern.
From modules to packages: when one file becomes a folder
A single module works well for a handful of related functions. But real projects outgrow one file. Think of your school bag: you do not carry every subject's notes as one giant loose bundle of pages — you organize them into separate notebooks, and the notebooks together live inside one bag. A Python package works the same way: it is a folder that groups several related modules together, and that folder itself becomes something you can import as one unit.
To make a folder into a package, it needs a special file inside it named __init__.py. That file's presence is what tells Python "this folder is not just a collection of loose files — treat it as one importable package." Build one now. Create a folder named school_tools, and inside it place three files.
school_tools/marks.py:
def average(marks_list):
return sum(marks_list) / len(marks_list)
school_tools/attendance.py:
def average(present, total):
return (present / total) * 100
school_tools/__init__.py:
print("Loading school_tools package...")
Notice, again, both submodules define a function called average — and again, that is completely fine, for the same namespace reason as before, now one level deeper. From outside the folder, write main.py:
import school_tools.marks
import school_tools.attendance
import school_tools
print(school_tools.marks.average([85, 90, 78, 92]))
print(school_tools.attendance.average(200, 220))
Trace this line by line, because the order of what prints is easy to get wrong. The very first import, import school_tools.marks, needs to load the school_tools package before it can reach into marks, so Python runs __init__.py first — printing Loading school_tools package... — and then loads marks.py. The second import, import school_tools.attendance, finds that school_tools is already cached (as you now know from the previous section), so __init__.py does not print again; only the new submodule, attendance.py, loads. The third import, import school_tools alone, finds the whole package already cached and does nothing visible at all. So across three import lines, the loading message appears exactly once, at the very top of the output. Then school_tools.marks.average([85, 90, 78, 92]) computes (85 + 90 + 78 + 92) / 4 = 345 / 4 = 86.25. Finally, school_tools.attendance.average(200, 220) computes (200 / 220) * 100, which is approximately 90.909090909... — Python prints the full float rather than rounding it for you, which is itself worth remembering: division in Python does not round automatically, and if you want two decimal places for a report you must ask for it explicitly, for instance with round(90.909090909, 2).
The __init__.py file can do more than print a message — in real packages it commonly pre-imports the submodules people will want, so users can write just import school_tools and immediately reach school_tools.marks.average(...) without separately importing school_tools.marks. That is done by placing lines like from . import marks and from . import attendance inside __init__.py itself — the leading dot means "a module in this same package."
Exploring a module: dir() and help()
Once a module is imported, you can inspect what it offers without opening its source file. dir(geometry) returns a list of every name defined inside it — your area_of_circle and area_of_rectangle will be in that list, alongside several names Python adds automatically to every module (these have double underscores on both sides, like __name__, and are usually not what you are looking for). help(geometry.area_of_circle) prints the function's signature together with its docstring — the triple-quoted string placed as the first line inside the function — which is exactly why writing a one-line docstring for every function you define is good habit: it turns your own module into something a classmate (or you, six months later) can understand with help() instead of by re-reading the whole file.
Modules you write versus modules you install: what "library" means
Common misconception — "module, package, and library all mean the same thing." They are related but distinct. A module is one file. A package is a folder of modules with an __init__.py. A library is the general, informal name for any reusable collection of code meant to be shared — it might be a single module, or (far more commonly) a package, or even a whole family of packages. When people say "the Python standard library," they mean the large collection of ready-made modules and packages — math, random, statistics, datetime, and hundreds more — that installs automatically with Python itself, so import math works with nothing extra to set up. Beyond the standard library, there are libraries that do not ship with Python but can be added using a tool called pip (Python's package installer), with a command such as pip install numpy at the terminal — numpy and pandas are real, widely used libraries for numerical and tabular data work, built by the open-source community. Structurally, a downloaded library like numpy is built exactly the way you built school_tools above: folders, __init__.py files, and modules full of functions — just written by many people, tested extensively, and distributed for others to install. When this chapter's title says "building your own libraries," it means precisely this: the geometry.py module and the school_tools package you built above are, structurally, miniature libraries — the same idea that scales up to numpy, just smaller and written by you.
Package structure and import resolution, visually
Practice: trace before you run
Work through each question on paper first, then check by running the code, so the habit of tracing (not guessing) sticks before the exam room.
- Given
utils.pycontaining onlydef double(n): return n * 2, and a second file withfrom utils import doublefollowed byprint(double(9)), what prints, and why is the module prefix not needed here? - Two files,
a.pywithdef info(): return "A"andb.pywithdef info(): return "B", are both imported intomain.pyusingimport aandimport b. What doesprint(a.info(), b.info())print, and what would go wrong if both were imported instead withfrom a import *followed byfrom b import *? - A file
stats.pystarts withprint("stats loaded")before any function definitions. If a program importsstatsthree separate times across three different files, how many times does "stats loaded" actually print, and what mechanism causes that? - A file contains a function definition followed by
if __name__ == "__main__": print("self-test"). State what__name__equals, and whether "self-test" prints, in each of these two cases: (a) the file is run directly withpython file.py; (b) the file is imported by another file withimport file. - For a package named
toolkitcontainingtoolkit/__init__.pyandtoolkit/helper.py, what is the minimum requirement inside the folder that makes Python treat it as an importable package rather than just a folder of loose files? - Explain, in your own words, the difference between a module, a package, and a library — and give one example of each from this chapter.
Answer check: (1) prints 18; no prefix is needed because from utils import double copies the name double directly into the importing file's own namespace. (2) prints A B; with two star-imports, b's info would silently overwrite a's info, so calling info() afterward would always give "B", with no error to warn you. (3) prints once; Python caches an imported module after its first load and reuses that cached copy for every later import in the same running program. (4) (a) __name__ equals "__main__", so "self-test" prints; (b) __name__ equals the module's own name (e.g. "file"), so it does not print. (5) an __init__.py file inside the folder. (6) a module is one .py file; a package is a folder of modules with an __init__.py; a library is the general term for reusable code meant to be shared, whether it is one module (math) or a whole package (school_tools, or an installed one like numpy).
Summary
A module is simply a .py file, and importing it runs its top-level code exactly once, caching the result for every later import in the same program. The dot in module_name.function_name is not decoration — it is a namespace lookup, and it is precisely what lets two different files define a function with the same name, such as average or total, without ever colliding. from module import * throws away that protection by copying every name directly into your file, which is why it should be avoided in favour of import module_name or a deliberately named from module import specific_thing. The same file can serve as both a runnable script and an importable module, distinguished only by whether __name__ equals "__main__". When a set of related modules grows large, organizing them into a folder with an __init__.py turns that folder into a package, importable as one unit and internally still protected by the same per-module namespaces. Every large, professionally built Python library — the standard library's own modules, or installed ones like numpy — is built from exactly these two ingredients: files that are modules, and folders of files that are packages. The geometry.py file and the school_tools package built in this chapter are not toy examples of a different, simpler idea — they are the real thing, at a small scale you can hold in your head completely.