Modules, Packages & Virtual Environments
Organize Python projects. Learn import systems, module resolution, package architectures, pip installations, and virtualenv setups.
Downloaded from PrepIQ (https://prepiq.online)
Q1. What is the difference between a module and a package in Python?
| Term | Definition |
|---|---|
| Module | A single .py file containing Python code (functions, classes, variables) |
| Package | A directory containing multiple modules, with an __init__.py file marking it as a package |
# math_utils.py - this file IS a module
def add(a, b):
return a + b
def subtract(a, b):
return a - b
# Using the module
import math_utils
print(math_utils.add(2, 3)) # 5
# Package structure example:
# myapp/
# __init__.py
# utils/
# __init__.py
# math_utils.py
# string_utils.py
# models/
# __init__.py
# user.py
# Importing from a package
from myapp.utils.math_utils import add
from myapp.models.user import User
# Since Python 3.3, __init__.py is technically optional for 'namespace packages',
# but explicit __init__.py is still recommended for regular packages
# for clarity and to control what gets exposed on import
Q2. What is __init__.py used for in Python packages?
__init__.py marks a directory as a Python package and runs automatically when the package is first imported. It can be empty, or used to control what's exposed, run setup code, or simplify import paths.
# myapp/utils/__init__.py
# Empty file - still makes 'utils' a valid package
# OR expose specific items directly at the package level
from .math_utils import add, subtract
from .string_utils import capitalize_words
# This allows shorter imports for users of the package:
# from myapp.utils import add (instead of myapp.utils.math_utils.add)
# Controlling 'from package import *' behavior
__all__ = ['add', 'subtract', 'capitalize_words']
# only these names are imported when someone does 'from myapp.utils import *'
# Package-level initialization code (runs once, on first import)
print('Initializing utils package')
DEFAULT_CONFIG = {'version': '1.0'}
# Usage from outside
from myapp.utils import add # works because __init__.py exposed it
print(add(2, 3)) # 5
# Without the re-export in __init__.py, you'd need the longer path:
# from myapp.utils.math_utils import add
Q3. What is the difference between absolute imports and relative imports in Python?
| Type | Syntax | Based on |
|---|---|---|
| Absolute import | from myapp.utils import helper | Full path from the project root |
| Relative import | from .utils import helper / from ..models import User | Position relative to the current module |
# Project structure:
# myapp/
# __init__.py
# main.py
# utils/
# __init__.py
# helpers.py
# models/
# __init__.py
# user.py
# ABSOLUTE import (in models/user.py) - full path from project root
from myapp.utils.helpers import format_name
# RELATIVE import (in models/user.py) - relative to current module's location
from ..utils.helpers import format_name # .. means 'go up one package level'
from . import validators # . means 'current package'
# Single dot (.) = current package
# Double dot (..) = parent package
# Triple dot (...) = grandparent package, and so on
# Absolute imports are generally PREFERRED because they're:
# - explicit and unambiguous about where something comes from
# - easier to understand when reading code out of context
# - don't break as easily when files are moved within the package
# Relative imports are useful for:
# - large packages where the full path is long and repetitive
# - keeping a package portable/renamable without updating every import
# IMPORTANT: relative imports only work inside a package (won't work
# if you run the file directly as a script, only when imported as a module)
Q4. What is a virtual environment and why should you use one?
A virtual environment is an isolated Python installation with its own set of packages, separate from the system-wide Python and other projects - preventing dependency conflicts between projects.
# Creating a virtual environment (built into Python 3.3+)
python -m venv myenv
# Activating it
# On Windows:
myenv\Scripts\activate
# On macOS/Linux:
source myenv/bin/activate
# Once activated, pip installs go into THIS environment only
pip install requests
pip list # shows only packages installed in this venv
# Deactivating
deactivate| Without venv | With venv |
|---|---|
| All projects share one global package set | Each project has its own isolated packages |
| Project A needs Django 3, Project B needs Django 4 - CONFLICT | Each project can use different versions freely |
| Risk of breaking system Python tools | System Python stays untouched |
# Saving and restoring exact dependencies
pip freeze > requirements.txt # export current packages and versions
pip install -r requirements.txt # install exact same packages elsewhere
# Alternative tools: virtualenv (more features), conda, poetry, pipenv
Q5. What is the difference between pip install and pip install -e (editable install)?
| Command | Behavior |
|---|---|
| pip install package | Copies a snapshot of the package's code into site-packages |
| pip install -e . | Links to the LOCAL source directory - changes to the code are reflected immediately, no reinstall needed |
# Regular install - copies package files
pip install requests
# Code lives in site-packages, editing it there has no effect on new environments
# Editable install - for developing your OWN package locally
# Given a project structure with setup.py or pyproject.toml:
# my_package/
# setup.py
# my_package/
# __init__.py
# core.py
pip install -e . # installs a LINK to this directory, not a copy
# Now editing my_package/core.py takes effect IMMEDIATELY
# in any other code that imports my_package, without reinstalling
import my_package
my_package.core.some_function() # reflects the latest saved changes
# Why this matters: during development of a library, you'd otherwise
# have to reinstall after every change to test it in a dependent project
# Minimal setup.py needed for editable installs
# setup.py:
# from setuptools import setup, find_packages
# setup(name='my_package', version='0.1', packages=find_packages())
Q6. How does Python find and import modules? What is sys.path?
When you import a module, Python searches through a list of directories stored in sys.path, in order, until it finds a matching module or package.
import sys
print(sys.path)
# Typical output includes:
# ['', '/usr/lib/python3.x', '/usr/lib/python3.x/site-packages', ...]
# Search order (roughly):
# 1. The directory of the script being run (or '' for current directory)
# 2. PYTHONPATH environment variable directories (if set)
# 3. Standard library directories
# 4. site-packages (where pip installs third-party packages)
# Adding a directory to sys.path at runtime (not usually recommended,
# but useful for scripts or debugging)
import sys
sys.path.append('/path/to/my/modules')
import my_custom_module # now findable
# ModuleNotFoundError happens when Python searches ALL of sys.path
# and doesn't find a matching module/package anywhere
# import nonexistent_module # ModuleNotFoundError: No module named 'nonexistent_module'
# Checking where a specific module was loaded from
import json
print(json.__file__) # shows the actual file path used
# Import caching - a module is only executed ONCE per process,
# even if imported multiple times from different files
import sys
print('mymodule' in sys.modules) # True if already imported somewhere
Q7. What is the difference between requirements.txt and pyproject.toml?
| File | Purpose | Format |
|---|---|---|
| requirements.txt | Simple list of dependencies (traditional pip approach) | Plain text, one package per line |
| pyproject.toml | Modern, standardized project metadata AND dependencies (PEP 518/621) | TOML format, structured |
# requirements.txt example
# requests==2.31.0
# flask>=2.0,<3.0
# pytest
pip install -r requirements.txt
# pyproject.toml example - more structured, covers build system too
# [project]
# name = "my_package"
# version = "1.0.0"
# dependencies = [
# "requests==2.31.0",
# "flask>=2.0,<3.0"
# ]
#
# [project.optional-dependencies]
# dev = ["pytest", "black"]
#
# [build-system]
# requires = ["setuptools>=61.0"]
# build-backend = "setuptools.build_meta"
# pyproject.toml is the modern STANDARD replacing setup.py AND
# often requirements.txt for defining a package's build configuration,
# metadata, and dependencies all in one standardized file
# Tools like poetry and pip-tools generate/manage pyproject.toml
# for reproducible dependency resolution
Q8. What is the difference between import module and from module import name?
| Style | Access pattern | Namespace |
|---|---|---|
| import module | module.name | Keeps names in the module's own namespace |
| from module import name | name (directly) | Brings the name into the current namespace |
# import module - must prefix with module name
import math
print(math.sqrt(16)) # 4.0
print(math.pi) # 3.14159...
# from module import name - use directly, no prefix
from math import sqrt, pi
print(sqrt(16)) # 4.0
print(pi) # 3.14159...
# from module import * - imports EVERYTHING (generally discouraged)
from math import *
print(sqrt(16)) # works, but unclear WHERE sqrt came from when reading code
# Risk: name collisions if two modules define the same name
# import module as alias - common for long/frequently-used module names
import numpy as np
import pandas as pd
print(np.array([1, 2, 3]))
# from module import name as alias
from datetime import datetime as dt
print(dt.now())
# Best practice guidance:
# - 'import module' is clearer about WHERE functions come from (module.func())
# - 'from module import specific_name' is fine for a few well-known names
# - avoid 'import *' in production code - it hides where names originate
# and can silently override existing names in your namespace
Q9. What is the __name__ == '__main__' idiom used for?
This idiom lets a Python file work both as a standalone script AND as an importable module, by checking whether the file is being run directly or imported elsewhere.
# calculator.py
def add(a, b):
return a + b
def subtract(a, b):
return a - b
def main():
print(add(2, 3))
print(subtract(5, 2))
if __name__ == '__main__':
main() # only runs when calculator.py is executed DIRECTLY
# Running directly:
# python calculator.py
# Output: 5, 3 (main() runs)
# Importing it elsewhere:
# other_file.py
import calculator
print(calculator.add(10, 20)) # 30
# main() does NOT run here - just the add/subtract functions are used
# Why __name__ is different in each case:
# - When a file is run directly, Python sets its __name__ to '__main__'
# - When a file is imported, Python sets its __name__ to the module's actual name
print(__name__) # '__main__' if run directly, 'calculator' if imported
# This pattern is essential for writing testable, reusable modules -
# it lets you put demo/test code in the same file without it running
# every time the module's functions are imported elsewhere
Q10. What is a namespace package, and how do circular imports happen?
A namespace package (PEP 420) is a package split across multiple directories without an __init__.py, useful for large plugin-style systems. Circular imports happen when two modules import each other, directly or indirectly.
# Circular import example
# a.py
import b
def func_a():
return b.func_b()
# b.py
import a # a is not fully loaded yet when this runs!
def func_b():
return a.func_a()
# import a # ImportError: cannot import name 'func_a' from partially initialized module 'a'
# Why it happens: when a.py starts executing, Python marks it as
# 'being imported' and starts running its code. It hits 'import b',
# which then hits 'import a' - but 'a' isn't finished loading yet,
# so some of its names may not exist yet
# FIX 1: import inside the function (deferred import, avoids the issue at load time)
# a.py
def func_a():
import b # imported only when func_a() is actually called, by which time both modules are loaded
return b.func_b()
# FIX 2: restructure code to avoid the circular dependency entirely
# (e.g. move shared logic to a third module both can import from)
# shared.py
def shared_logic():
pass
# a.py and b.py both import from shared.py instead of each other
# Namespace package example (PEP 420) - no __init__.py needed
# company/
# plugin_a/
# module1.py
# plugin_b/
# module2.py
# Both can be installed separately but combined under 'company' namespace
Modules, Packages & Virtual Environments
Organize Python projects. Learn import systems, module resolution, package architectures, pip installations, and virtualenv setups.
What is the difference between a module and a package in Python?
TermDefinitionModuleA single .py file containing Python code (functions, classes, variables)PackageA directory...
What is __init__.py used for in Python packages?
__init__.py marks a directory as a Python package and runs automatically when the package is first imported. I...
What is the difference between absolute imports and relative imports in Python?
TypeSyntaxBased onAbsolute importfrom myapp.utils import helperFull path from the project rootRelative importf...
What is a virtual environment and why should you use one?
A virtual environment is an isolated Python installation with its own set of packages, separate from the syste...
What is the difference between pip install and pip install -e (editable install)?
CommandBehaviorpip install packageCopies a snapshot of the package's code into site-packagespip install -e .Li...
How does Python find and import modules? What is sys.path?
When you import a module, Python searches through a list of directories stored in sys.path, in order, until it...
What is the difference between requirements.txt and pyproject.toml?
FilePurposeFormatrequirements.txtSimple list of dependencies (traditional pip approach)Plain text, one package...
What is the difference between import module and from module import name?
StyleAccess patternNamespaceimport modulemodule.nameKeeps names in the module's own namespacefrom module impor...
What is the __name__ == '__main__' idiom used for?
This idiom lets a Python file work both as a standalone script AND as an importable module, by checking whethe...
What is a namespace package, and how do circular imports happen?
A namespace package (PEP 420) is a package split across multiple directories without an __init__.py, useful fo...