Week 11: Python Modules & CLI Arguments

4077 words
20 min read
2026-09-12T00:00:00.000Z
Python Week 1: the first filter for runtime behavior
Visual companion
Python
Type and operator map

Python Week 1: the first filter for runtime behavior

View
Revision summary

What this note is really saying

Short form

Creating and importing user modules, import variations, namespace separation, __name__ == '__main__', and sys.argv. # Programming in Python — Week 11: Python Modules & CLI Arguments The official IITM syllabus lists **Week 11: Module System**.

Programming in Python — Week 11: Python Modules & CLI Arguments

The official IITM syllabus lists Week 11: Module System.
This week is about organizing Python code so that useful functions, variables, and classes can be reused across files instead of rewriting everything in one program.

TL;DR

A Python file can act as a module.
Example:
text
math_utils.py
main.py
math_utils.py:
python
def square(x):
    return x * x
main.py:
python
import math_utils

print(math_utils.square(5))
The main ideas are:
  • modules
  • import
  • module namespaces
  • from ... import ...
  • aliases with as
  • standard-library modules
  • your own modules
  • __name__
  • if __name__ == "__main__":
  • packages/directories conceptually
  • avoiding name conflicts
  • separating reusable code from program-specific code
Core mental model:
module=Python file containing reusable definitions\boxed{ \text{module} = \text{Python file containing reusable definitions} }

1. Why modules?

Suppose one program contains:
python
def mean(...):
    ...

def median(...):
    ...

def variance(...):
    ...
Later another program needs the same functions.
Without modules, you might copy-paste them.
That creates problems:
  • duplicate code
  • inconsistent fixes
  • harder maintenance
  • larger files
  • poor organization
Modules let you write reusable code once.

2. Basic module

Suppose file:
text
mymath.py
contains:
python
def square(x):
    return x ** 2

def cube(x):
    return x ** 3
Another file can import it.

3. import module

python
import mymath
Now use:
python
mymath.square(4)
Result:
16.16.
And:
python
mymath.cube(3)
gives:
27.27.

4. Why prefix with module name?

After:
python
import mymath
Python keeps definitions associated with the module namespace.
So:
python
mymath.square
means:
the name square belonging to module mymath.
This helps prevent collisions between similarly named functions.

5. Namespace

A namespace is conceptually a mapping:
name→object\boxed{ \text{name}\rightarrow\text{object} }
Inside module mymath:
text
square → function
cube   → function
So:
python
mymath.square
looks up square in mymath's namespace.

6. Name collision example

Suppose:
python
def square(x):
    return "local"
and module also has:
python
mymath.square
Then:
python
square(5)
and:
python
mymath.square(5)
refer to different names.
Module qualification makes the distinction explicit.

7. from module import name

Instead of:
python
import mymath
you may write:
python
from mymath import square
Now call:
python
square(5)
without module prefix.

8. Difference between the two styles

Style 1

python
import mymath
mymath.square(5)

Style 2

python
from mymath import square
square(5)
Both can access the same function.
The first makes origin clearer.

9. Import several names

python
from mymath import square, cube
Then:
python
square(4)
cube(3)

10. Aliasing with as

You can rename imported modules:
python
import math as m
Then:
python
m.sqrt(25)
returns:
5.5.

11. Why aliases?

Aliases can:
  • shorten long names
  • follow conventions
  • avoid collisions
Example:
python
import numpy as np
is a widely used convention, though NumPy appears formally in Week 12.

12. Alias imported function

python
from mymath import square as sq
Then:
python
sq(5)
returns:
25.25.

13. Standard library

Python includes many modules in its standard library.
Examples include modules for:
  • mathematics
  • random values
  • filesystem operations
  • dates/times
  • statistics
  • collections
These can be imported without writing them yourself.

14. math module

Example:
python
import math
Then:
python
math.sqrt(16)
math.floor(3.8)
math.ceil(3.2)

15. Constants from modules

python
import math

print(math.pi)
A module can contain:
  • functions
  • constants
  • classes
  • other objects
Not only functions.

16. Import specific mathematical names

python
from math import sqrt, pi
Then:
python
print(sqrt(25))
print(pi)

17. random module intuition

A standard module may provide reusable functionality such as random number generation.
Example:
python
import random
Then functions are accessed through:
python
random....
Exact APIs are secondary to the module-system idea:
functionality can live in another namespace and be imported.

18. Your own module

Suppose:
text
geometry.py
contains:
python
pi = 3.14159

def circle_area(r):
    return pi * r * r
Another file:
python
import geometry

print(geometry.circle_area(2))

19. Import executes module code

This point matters.
Suppose demo.py contains:
python
print("Module loaded")

x = 10
Then:
python
import demo
executes the module's top-level code.
So:
text
Module loaded
may print during import.

20. Definitions are executed too, but bodies wait

When Python sees:
python
def f():
    print("inside")
during module loading, it creates the function object.
It does not execute:
python
print("inside")
until:
python
f()
is called.

21. Top-level statements

These run during import:
python
x = 10
print("hello")
A function body:
python
def f():
    print("inside")
does not run simply because the module was imported.

22. Why module side effects matter

Suppose importing a helper module unexpectedly:
  • prints output
  • modifies files
  • asks for input
  • starts long computations
That makes reuse inconvenient.
Reusable modules usually keep top-level behavior controlled.

23. __name__

Every Python module has a special variable:
python
__name__
Its value depends on how the file is being used.

24. File run directly

If a Python file is executed as the main program:
python
__name__
is conventionally:
python
"__main__"

25. File imported as module

If:
text
helper.py
is imported:
python
import helper
then inside helper.py, __name__ is typically:
python
"helper"
rather than "__main__".

26. Main guard

A common pattern:
python
if __name__ == "__main__":
    ...
This means:
run this block only when this file is executed directly.

27. Example

geometry.py:
python
def square(x):
    return x * x

if __name__ == "__main__":
    print(square(5))
If run directly:
text
25
prints.
If imported:
python
import geometry
the test block does not execute.

28. Why main guard is useful

It lets one file serve two roles:
  1. reusable module
  2. executable/testable program
Reusable definitions stay available without unwanted top-level execution.

29. Conceptual structure

python
def helper1():
    ...

def helper2():
    ...

def main():
    ...

if __name__ == "__main__":
    main()
This cleanly separates:
  • definitions
  • application entry point

30. Module variables

Suppose:
python
## settings.py
tax_rate = 0.18
Another file:
python
import settings

price = 100
final = price * (1 + settings.tax_rate)
A module can act as a place for shared constants/configuration.

31. Module state

A module can also contain mutable state.
Example:
python
count = 0

def increment():
    global count
    count += 1
Imported modules are objects with their own namespace/state.
At an introductory level, avoid unnecessary shared mutable module state unless the design needs it.

32. Reusing functions

Suppose:
text
stats_utils.py
contains:
python
def mean(values):
    return sum(values) / len(values)
Now many programs can use:
python
import stats_utils

avg = stats_utils.mean(data)
This is code reuse without copy-paste.

33. Separation of concerns

A good design might use:
text
input_utils.py
statistics.py
reporting.py
main.py
Each module has one broad responsibility.
This reduces one huge file into understandable components.

34. Module boundaries as abstraction

Suppose statistics.py exposes:
python
mean(values)
The caller need not care how mean is implemented internally.
This connects back to Computational Thinking's abstraction ideas:
interface≠implementation\boxed{ \text{interface} \neq \text{implementation} }

35. Importing implementation details

Even though Python may allow access to many module names, good code should depend primarily on the module's intended public interface.
This reduces coupling.

36. Names beginning with underscore

Conventionally:
python
_helper()
_internal_value
suggests:
intended for internal module use.
This is mainly a convention, not strong privacy enforcement.

37. from module import *

Python allows:
python
from mymodule import *
This imports many public names directly.
But it is often discouraged because it can:
  • pollute namespace
  • create name collisions
  • hide where names came from

38. Why explicit imports are clearer

Compare:
python
from math import *
print(sqrt(25))
with:
python
import math
print(math.sqrt(25))
The second makes the source of sqrt obvious.

39. Name shadowing

Suppose:
python
from math import sqrt
then later:
python
sqrt = 5
Now:
python
sqrt(25)
fails because sqrt refers to integer 5.
Assignment can shadow imported names.

40. Module alias avoids collisions

Instead:
python
import math
then:
python
sqrt = 5
print(math.sqrt(25))
still works because module namespace keeps math.sqrt separate.

41. Import order conceptually

Python must locate the requested module.
For your own module:
python
import helper
Python needs to find an appropriate helper.py or package/module on its import search path.
For basic course exercises, files may simply reside in the same working directory.

42. File name matters

If module file is:
text
geometry.py
typically:
python
import geometry
uses the filename without .py.
Not:
python
import geometry.py

43. Avoid naming your files after standard modules

Suppose you create:
text
math.py
and then write:
python
import math
Python may import your local file instead of the standard library module.
This can cause confusing errors.

44. Circular imports intuition

Suppose:
text
A.py imports B.py
B.py imports A.py
This creates a circular dependency.
Such designs can produce partially initialized modules and confusing behavior.
At this level, the main lesson is:
modules should have clear dependency direction.

45. Dependency graph

You can visualize imports as:
text
main
├── input_utils
├── statistics
└── reporting
Prefer understandable dependency structure rather than everything importing everything else.

46. Package intuition

A package groups related modules in a directory structure.
Conceptually:
text
project/
    analytics/
        stats.py
        cleaning.py
        plotting.py
Then code can conceptually import related modules from the package.
Exact packaging details can vary; the key idea is hierarchical organization beyond a single module.

47. Module vs package

Module

Usually one Python file:
text
stats.py

Package

Collection/directory of related modules:
text
analytics/
    stats.py
    cleaning.py

48. Dot notation for packages

Conceptually:
python
import package.module
Then:
python
package.module.function()
The dots represent nested namespaces.

49. Reusing across programs

Suppose three programs need:
python
validate_mark(x)
Instead of copying function three times:
text
program1.py
program2.py
program3.py
put it once in:
text
validation.py
and import it.

50. Maintenance benefit

If validation rule changes from:
0≤x≤1000\leq x\leq100
to another rule, update one function in one module.
Every program using that module can benefit from the corrected implementation.
This is a major software-engineering advantage.

51. Testing modules independently

A reusable module can be tested independently from the application that uses it.
For example:
python
if __name__ == "__main__":
    print(mean([1, 2, 3]))
or through separate tests.
This isolates bugs.

52. Import only what you need

Compare:
python
import math
with:
python
from math import sqrt
The first imports module namespace.
The second places sqrt directly in current namespace.
Choose based on clarity and usage.

53. Module-qualified call

python
math.sqrt(16)
communicates two things:
  • operation: sqrt
  • source: math
This extra qualification can improve readability.

54. Function imported directly

python
sqrt(16)
is shorter but context may be less obvious.
Neither form is universally superior.

55. Alias conventions

Some libraries have conventional aliases, such as:
python
import numpy as np
import pandas as pd
These conventions reduce verbosity while remaining recognizable.
Week 12 will make those particularly relevant.

56. Multiple imports

Possible:
python
import math
import random
or:
python
import math, random
The first style is often clearer and easier to maintain.

57. Import placement

Imports are commonly placed near the top of a Python file.
Example:
python
import math
import statistics

def main():
    ...
This makes dependencies visible immediately.

58. Import inside function

Python also allows:
python
def f():
    import math
    return math.sqrt(25)
But imports are commonly top-level unless there is a reason to localize them.

59. Standard-library reuse

Using an existing tested module is often preferable to rewriting functionality manually.
Example:
Instead of implementing square root yourself, use:
python
math.sqrt(...)
when appropriate.
This is abstraction and reuse.

60. Module documentation intuition

A module should make clear:
  • what it provides
  • what arguments functions expect
  • what values functions return
  • any assumptions
This makes it usable without reading every implementation line.

61. Docstrings

A function/module may contain documentation strings:
python
def mean(values):
    """Return arithmetic mean of non-empty values."""
    return sum(values) / len(values)
Docstrings are associated with the object and can be inspected by tools.

62. Module-level docstring

At top of file:
python
"""Utility functions for numerical analysis."""
This documents the module as a whole.

63. Public interface intuition

Suppose module exposes:
python
mean
median
mode
A user of the module should conceptually think:
text
inputs
→ function
→ output
without needing implementation details.
That is a software abstraction boundary.

64. Import and mutable objects

Suppose module defines:
python
values = []
and another file imports module:
python
import helper
helper.values.append(5)
The module-level list is mutable shared state.
Other code accessing helper.values sees the mutation.
This can be useful, but can also make reasoning harder.

65. Prefer functions over uncontrolled global state

Instead of exposing:
python
counter = 0
and letting everyone modify it, cleaner design may provide:
python
def increment():
    ...
This preserves a more controlled interface.
The same encapsulation idea appears again.

66. Import caching intuition

Within one running Python process, importing the same module repeatedly does not normally execute its top-level code from scratch every single time.
Python keeps track of loaded modules.
The key practical lesson:
do not design module correctness around repeated-import side effects.

67. Example import side effect

demo.py:
python
print("loaded")
Main:
python
import demo
import demo
You should not assume "loaded" prints twice merely because two import statements appear.

68. __name__ trace

helper.py:
python
print(__name__)

Run directly

Conceptually prints:
text
__main__

Imported using

python
import helper
conceptually prints:
text
helper
This explains the main-guard pattern.

69. Main function pattern

python
def main():
    print("Program starts here")

if __name__ == "__main__":
    main()
This makes program entry explicit.

70. Why define main()?

Instead of placing lots of logic at top-level, main():
  • organizes application flow
  • reduces import side effects
  • makes testing easier
  • separates definitions from execution

71. Example two-file program

utils.py:
python
def is_even(n):
    return n % 2 == 0
main.py:
python
import utils

n = int(input())

if utils.is_even(n):
    print("Even")
else:
    print("Odd")

72. Execution flow

When main.py reaches:
python
import utils
Python loads utils.
Then:
python
utils.is_even
becomes available.
Later:
python
utils.is_even(n)
calls that function.

73. Function arguments still behave normally

The fact that a function came from a module does not change basic function semantics.
python
utils.is_even(n)
passes n as an argument exactly like a locally defined function call.

74. Module-local variables

Suppose utils.py:
python
threshold = 50

def passed(mark):
    return mark >= threshold
Inside passed, threshold is looked up in the module's global namespace.
Another module can use:
python
utils.threshold
if exposed.

75. Global means module-global here

In Python, a global variable inside a module is generally global to that module's namespace, not magically shared as an unqualified name everywhere.
For example:
python
utils.threshold
is distinct from a separate:
python
threshold
in main.py.

76. Same variable name in two modules

A.py:
python
x = 10
B.py:
python
x = 20
Main:
python
import A
import B

print(A.x)
print(B.x)
outputs:
text
10
20
Namespaces keep them separate.

77. from ... import ... can create conflict

python
from A import x
from B import x
After the second import, unqualified:
python
x
refers to whichever binding currently occupies that name—typically the later imported one.
This is why qualified module names can be safer.

78. Alias to disambiguate

python
from A import x as ax
from B import x as bx
Now:
python
ax
bx
remain distinct.

79. Import errors

If Python cannot find a requested module:
python
import does_not_exist
you get an import-related error such as:
text
ModuleNotFoundError
This is different from a logical error inside your code.

80. Import succeeds but attribute absent

Suppose:
python
import math
but you ask for:
python
math.nonexistent
The module exists, but that attribute does not.
This is a different failure from the module itself being missing.

Common mistake: include
.py
Wrong:
python
import helper.py
Normal module import:
python
import helper

Common mistake: forget module prefix
After:
python
import math
wrong:
python
sqrt(25)
unless sqrt was separately imported.
Correct:
python
math.sqrt(25)

Common mistake: use prefix after direct import
After:
python
from math import sqrt
normally call:
python
sqrt(25)
Not:
python
math.sqrt(25)
unless math itself was also imported.

Common mistake: wildcard imports
python
from module import *
may silently overwrite names and obscure origins.
Prefer explicit imports for predictable code.

Common mistake: execute program on import
Suppose module ends with:
python
main()
Then importing it runs the program.
Better:
python
if __name__ == "__main__":
    main()
when execution should happen only directly.

Common mistake: duplicate code instead of extracting module
If several programs contain identical helper functions, that is a sign reusable logic could be placed into a common module.

Common mistake: giant utility module
The opposite extreme is one file containing hundreds of unrelated helpers.
Good modularization groups related functionality coherently.

Common mistake: circular dependency
Avoid architectures like:
text
A imports B
B imports A
when possible.
Prefer a dependency structure such as:
text
A ──> shared
B ──> shared
where common functionality moves into a third module.

Common mistake: shadow standard modules
Files named:
text
math.py
random.py
statistics.py
can interfere with intended standard-library imports.
Choose project filenames carefully.

Common mistake: shadow imported name
python
import math

math = 10
Now:
python
math.sqrt(4)
fails because math no longer refers to the module.

91. Trace example

helper.py:
python
x = 10

def add(a, b):
    return a + b
main.py:
python
import helper

y = helper.add(helper.x, 5)

print(y)
Trace:
text
helper.x = 10
helper.add(10, 5)
→ 15
Output:
15\boxed{15}

92. Trace with alias

python
import helper as h

print(h.add(2, 3))
h refers to the module object.
Then:
python
h.add
retrieves its function.
Output:
5.5.

93. Trace direct import

python
from helper import add

print(add(2, 3))
The name add is placed directly in the current module's namespace.
No helper. prefix is needed.

94. Main-guard trace

demo.py:
python
def f():
    print("F")

print("A")

if __name__ == "__main__":
    print("B")

Run directly

Output:
text
A
B

Import as module

Top-level:
python
print("A")
still executes.
But main-guard body does not.
Output:
text
A

95. Why definitions alone are safer

If reusable module mostly contains:
python
def ...
class ...
CONSTANT = ...
then importing it has predictable behavior.
Heavy application logic belongs behind explicit function calls or a main guard.

96. Modules and files are different concepts

Week 9–10 files:
text
data.txt
are generally data.
Week 11 modules:
text
utils.py
contain Python code.
Both are files at the operating-system level, but their roles differ.

97. Module system as code organization

Think:
text
DATA ORGANIZATION
lists / dicts / files

CODE ORGANIZATION
functions / modules / packages
Functions organize instructions inside a file.
Modules organize functions/classes/constants across files.
Packages organize modules into larger groups.

98. Hierarchy of abstraction

text
individual statements
        ↓
functions
        ↓
modules
        ↓
packages
        ↓
applications
Each level groups lower-level details into more manageable units.

99. Week 10 → Week 11 transition

Week 10 asked:
How should data move between persistent files and Python programs?
Week 11 asks:
How should the Python program itself be organized?
Conceptually:
data modularity→code modularity\boxed{ \text{data modularity} \rightarrow \text{code modularity} }

100. Compact module cheat sheet

python
## import module
import math

math.sqrt(25)
python
## import specific name
from math import sqrt

sqrt(25)
python
## alias module
import math as m

m.sqrt(25)
python
## alias imported name
from math import sqrt as sq

sq(25)
python
## main guard
if __name__ == "__main__":
    main()

101. Import-style comparison

SyntaxCall styleNamespace clarity
import mathmath.sqrt()High
import math as mm.sqrt()High, shorter
from math import sqrtsqrt()Short
from math import *sqrt()Low; collision-prone

102. Module-design checklist

When deciding what belongs in a module, ask:
  1. Is this functionality reusable?
  2. Are these functions conceptually related?
  3. Can callers use them through a clean interface?
  4. Does importing the module avoid unnecessary side effects?
  5. Are names clear?
  6. Are dependencies understandable?
  7. Could this code be tested separately?
  8. Am I duplicating functionality somewhere else?

103. What you should actually master

By the end of Python Week 11, you should be able to:
  1. Explain what a Python module is.
  2. Import a module with import.
  3. Access module attributes with dot notation.
  4. Use from ... import ....
  5. Use aliases with as.
  6. Explain what a namespace is.
  7. Understand how modules reduce name collisions.
  8. Use standard-library modules conceptually.
  9. Create and import your own module.
  10. Understand that top-level module code executes during import.
  11. Distinguish function definition from function execution.
  12. Explain __name__.
  13. Explain __name__ == "__main__".
  14. Use a main guard to avoid unwanted import-time execution.
  15. Understand module-level constants/state.
  16. Distinguish modules from packages.
  17. Understand basic dependency structure.
  18. Avoid wildcard-import/name-shadowing problems.
  19. Recognize circular-import problems conceptually.
  20. Understand modules as reusable abstraction boundaries.
If these are comfortable, Programming in Python Week 11 is under control.

Quick Recall & Flashcards

Test your active recall before proceeding to practice problems. Format:
Question :: Answer.
  • Week 11 Diagnostic: What is the purpose of the if __name__ == '__main__': idiom in Python scripts? :: Answer: B — __name__ is '__main__' only when the script is run directly. When imported into another script, __name__ is set to the module name.
  • Week 11 Diagnostic: If a script is run as python script.py data.csv 100, what is the value of sys.argv[1]? :: Answer: B — sys.argv[0] is the script name 'script.py'; sys.argv[1] is the first CLI argument 'data.csv'.

Practice & Self-Check

Attempt these diagnostic questions to verify your operational understanding.
1

Question 1

What is the purpose of the if __name__ == '__main__': idiom in Python scripts?
MCQ
3 Unit Assessment
A. To check if the Python version is up to date
B. To execute code only when the file is run directly, and not when it is imported as a module
C. To define a class constructor
D. To grant root permissions
2

Question 2

If a script is run as python script.py data.csv 100, what is the value of sys.argv[1]?
MCQ
3 Unit Assessment
A. 'script.py'
B. 'data.csv'
C. '100'
D. 'python'

Active Recall Session

Document outline

Keep your place and jump directly to a heading.

Table of contents

No note selected

Open a chapter to see its outline, backlinks, and sources here.