Python Modules and Packages | import, pip, and Virtual Environments Explained

Python Modules and Packages | import, pip, and Virtual Environments Explained

이 글의 핵심

Hands-on guide to Python modules and packages: import patterns, pip, standard library highlights, virtual environments, and project layout.

Introduction

“Split code into modules”

Modules and packages are how you structure code and reuse it across projects.


1. What is a module?

Definition

A module is a .py file containing Python code. You can put functions, classes, and variables in a module and import them elsewhere.

Why use modules?

  • Reuse the same code in many places
  • Namespaces to avoid name clashes
  • Maintenance by splitting features into files

Creating a module

Example: a small math utilities module

# math_utils.py (module file)
"""
Math utility functions.
"""

def add(a, b):
    """Return a + b."""
    return a + b

def multiply(a, b):
    """Return a * b."""
    return a * b

def power(base, exponent):
    """Return base ** exponent."""
    return base ** exponent

# Module-level constants
PI = 3.14159
E = 2.71828

# Runs only when this file is executed directly
if __name__ == '__main__':
    print("math_utils self-test")
    print(f"2 + 3 = {add(2, 3)}")
    print(f"2 * 3 = {multiply(2, 3)}")

Using a module

# main.py (imports the module)
import math_utils

# Call functions on the module
result1 = math_utils.add(3, 5)
print(result1)  # 8

result2 = math_utils.multiply(4, 7)
print(result2)  # 28

# Module constants
print(math_utils.PI)  # 3.14159

# Circle area
radius = 5
area = math_utils.PI * math_utils.power(radius, 2)
print(f"Area of circle with radius {radius}: {area}")  # 78.53975

How __name__ works

# math_utils.py
print(f"__name__ = {__name__}")

if __name__ == '__main__':
    print("This module was run as the main program")
else:
    print("This module was imported")

# When you run `python math_utils.py`:
#   __name__ = __main__
#   This module was run as the main program

# When you `import math_utils`:
#   __name__ = math_utils
#   This module was imported

Module search path

Python looks for modules in this order:

import sys

for path in sys.path:
    print(path)

# Typical order:
# 1. Directory of the script you run
# 2. Entries from PYTHONPATH
# 3. Standard library directories
# 4. site-packages (third-party installs from pip)

# Add a path at runtime
sys.path.append('/custom/path')

2. import syntax

Four common patterns

There are four main ways to import. Each has trade-offs—pick what fits the situation.

1) Import the whole module

import math

print(math.sqrt(16))  # 4.0
print(math.pi)  # 3.141592653589793
print(math.factorial(5))  # 120

# Pros:
# - Clear provenance (math.sqrt comes from math)
# - Fewer name collisions

# Cons:
# - More typing (prefix every call)

When to use

  • You call several functions from the module
  • You want readers to see where names come from

2) Import specific names

from math import sqrt, pi, factorial

print(sqrt(16))  # 4.0 (no math. prefix)
print(pi)
print(factorial(5))

# Pros:
# - Shorter call sites
# - Import only what you need

# Cons:
# - Less obvious where names come from
# - Easier to shadow or clash names

Name clash example

from math import sqrt
from numpy import sqrt  # shadows math.sqrt!

print(sqrt(16))  # numpy.sqrt behavior (may differ from math)

# Fix: use aliases
from math import sqrt as math_sqrt
from numpy import sqrt as np_sqrt

print(math_sqrt(16))
print(np_sqrt(16))

When to use

  • You use a few names often and the context is obvious

3) Aliases

import math as m
import numpy as np
import pandas as pd

print(m.sqrt(16))
print(np.array([1, 2, 3]))
print(pd.DataFrame({'a': [1, 2]}))

# Pros:
# - Shorter names for long modules
# - Keeps namespace + less typing
# - Community conventions (np, pd) improve readability

# Cons:
# - Non-standard aliases confuse readers

Common aliases

import numpy as np          # ✅ idiomatic
import pandas as pd         # ✅ idiomatic
import matplotlib.pyplot as plt
import tensorflow as tf

import numpy as n           # ❌ non-standard
import pandas as p          # ❌ confusing

When to use

  • Long module names used many times
  • Libraries with a de-facto short name (numpy → np)

4) Star import (discouraged)

from math import *

print(sqrt(16))
print(pi)

# Pros:
# - Very short at call sites

# Cons:
# - Unclear origin of names
# - High collision risk
# - Harder code review
# - Conflicts with PEP 8 guidance

Problem scenario

from math import *
from statistics import *

# Both may define mean — which one wins?
result = mean([1, 2, 3, 4, 5])

When to use

  • Almost never in production (maybe in a quick REPL session)

Comparison table

StyleSyntaxExampleProsConsTypical use
Whole moduleimport mathmath.sqrt(16)ClearVerboseMany calls
From importfrom math import sqrtsqrt(16)ShortLess clearFew names
Aliasimport math as mm.sqrt(16)Clear + shortMust remember aliasLong names
Starfrom math import *sqrt(16)ShortestDangerousAvoid

Relative vs absolute imports

Absolute import: full path from the project/package root.

# Layout:
# myproject/
# ├── main.py
# └── mypackage/
#     ├── __init__.py
#     ├── utils.py
#     └── core/
#         └── engine.py

# In mypackage/core/engine.py
from mypackage.utils import helper  # absolute

Relative import: relative to the current package.

# In mypackage/core/engine.py
from ..utils import helper  # parent package
from . import config        # same package

# .  = current directory
# .. = parent
# ... = grandparent (rare)

Trade-offs

Absolute:
✅ Clear and stable
❌ Renaming the top-level package touches many imports

Relative:
✅ Easier when moving subpackages
❌ Fails if you run a module as a plain script (python engine.py)

Guideline: prefer absolute imports; use relative imports inside a package when it helps.


3. Packages

What is a package?

A package is a directory of modules. Traditionally an __init__.py file marks the directory as a package (and can run initialization code).

Why packages?

  • Hierarchy for related modules
  • Namespaces to avoid flat name clashes
  • Distribution as a single installable unit

Example layout

myproject/
├── main.py
├── requirements.txt
└── mypackage/
    ├── __init__.py
    ├── module1.py
    ├── module2.py
    └── subpackage/
        ├── __init__.py
        └── module3.py

# Python 3.3+ can treat directories as namespace packages without __init__.py,
# but an explicit __init__.py is still common for regular packages.

Building a package

Step 1: create modules

# mypackage/math_ops.py
def add(a, b):
    """Addition."""
    return a + b

def subtract(a, b):
    """Subtraction."""
    return a - b

# mypackage/string_ops.py
def reverse(text):
    """Reverse a string."""
    return text[::-1]

def capitalize_words(text):
    """Title-case each word."""
    return ' '.join(word.capitalize() for word in text.split())

Step 2: __init__.py

# mypackage/__init__.py
"""
mypackage: small utility library.
"""

from .math_ops import add, subtract
from .string_ops import reverse, capitalize_words

__version__ = '1.0.0'

# __all__ controls `from mypackage import *`
__all__ = ['add', 'subtract', 'reverse', 'capitalize_words']

# Optional side effect on import
print(f"mypackage v{__version__} loaded")

Step 3: use the package

# main.py
import mypackage

print(mypackage.add(3, 5))
print(mypackage.reverse("Hello"))

from mypackage.math_ops import add
from mypackage.string_ops import reverse

print(add(10, 20))
print(reverse("Python"))

from mypackage import add, reverse
print(add(1, 2))

Roles of __init__.py

# 1) Empty file: marks directory as a package
# mypackage/__init__.py
# (empty)

# 2) Re-export symbols for shorter imports
# from .math_ops import add
# enables: from mypackage import add

# 3) Package initialization: logging, settings
import logging
logging.basicConfig(level=logging.INFO)
logger = logging.getLogger(__name__)
logger.info("package init")

# 4) __all__ lists the public API
__all__ = ['add', 'subtract']

Subpackages

myproject/
├── main.py
└── mypackage/
    ├── __init__.py
    ├── math_ops.py
    ├── string_ops.py
    └── advanced/
        ├── __init__.py
        ├── statistics.py
        └── algorithms.py

Using a subpackage

# mypackage/advanced/statistics.py
def mean(numbers):
    """Arithmetic mean."""
    return sum(numbers) / len(numbers)

def median(numbers):
    """Median value."""
    sorted_nums = sorted(numbers)
    n = len(sorted_nums)
    mid = n // 2
    return sorted_nums[mid] if n % 2 == 1 else (sorted_nums[mid-1] + sorted_nums[mid]) / 2

# main.py
from mypackage.advanced.statistics import mean, median

data = [1, 2, 3, 4, 5]
print(f"mean: {mean(data)}")
print(f"median: {median(data)}")

4. The standard library

What is it?

The standard library ships with Python. No extra install—just import.

Frequently used modules

1) os — OS services

import os

print(os.getcwd())

os.chdir('/path/to/directory')

os.mkdir("new_folder")
os.makedirs("parent/child/grandchild")

print(os.path.exists('file.txt'))
print(os.path.isfile('file.txt'))
print(os.path.isdir('folder'))

path = os.path.join('folder', 'subfolder', 'file.txt')

print(os.environ.get('PATH'))
os.environ['MY_VAR'] = 'value'

files = os.listdir('.')
print(files)

2) datetime — dates and times

from datetime import datetime, timedelta, date, time

now = datetime.now()
print(now)

formatted = now.strftime("%Y-%m-%d %H:%M:%S")
print(formatted)

tomorrow = now + timedelta(days=1)
next_week = now + timedelta(weeks=1)
two_hours_ago = now - timedelta(hours=2)

print(f"Tomorrow: {tomorrow.strftime('%Y-%m-%d')}")
print(f"Next week: {next_week.strftime('%Y-%m-%d')}")

date_str = "2026-12-31 23:59:59"
parsed = datetime.strptime(date_str, "%Y-%m-%d %H:%M:%S")
print(parsed)

today = date.today()
print(today)

current_time = time(14, 30, 0)
print(current_time)

3) random — random numbers

import random

print(random.randint(1, 10))
print(random.randrange(0, 10, 2))

print(random.random())
print(random.uniform(1.0, 10.0))

colors = ['red', 'blue', 'green', 'yellow']
print(random.choice(colors))
print(random.choices(colors, k=3))
print(random.sample(colors, k=2))

numbers = [1, 2, 3, 4, 5]
random.shuffle(numbers)
print(numbers)

random.seed(42)
print(random.randint(1, 100))

4) json — JSON

import json

data = {"name": "Alice", "age": 25, "hobbies": ["reading", "movies"]}
json_str = json.dumps(data, ensure_ascii=False, indent=2)
print(json_str)

parsed = json.loads(json_str)
print(parsed['name'])
print(type(parsed))

with open('data.json', 'w', encoding='utf-8') as f:
    json.dump(data, f, ensure_ascii=False, indent=2)

with open('data.json', 'r', encoding='utf-8') as f:
    loaded = json.load(f)
    print(loaded)

5) sys — interpreter and runtime

import sys

print(sys.version)

# python script.py arg1 arg2
print(sys.argv)

print(sys.path)

# sys.exit(0)  # normal exit
# sys.exit(1)  # error exit

6) pathlib — object-oriented paths

from pathlib import Path

current = Path.cwd()
print(current)

file_path = Path('folder') / 'subfolder' / 'file.txt'
print(file_path)

if file_path.exists():
    print("exists")

file_path.write_text("Hello, World!", encoding='utf-8')
content = file_path.read_text(encoding='utf-8')
print(content)

print(file_path.name)
print(file_path.stem)
print(file_path.suffix)
print(file_path.parent)

for file in Path('.').glob('*.py'):
    print(file)

5. pip and package management

What is pip?

pip installs and manages third-party packages from PyPI (the Python Package Index).

Common pip commands

pip install requests

# pip will:
# 1. Find the package on PyPI
# 2. Download a release
# 3. Install dependencies
# 4. Place files under site-packages

pip install requests==2.28.0
pip install "requests>=2.28.0"
pip install "requests>=2.28.0,<3.0.0"

pip install --upgrade requests
pip install -U requests

pip show requests

pip uninstall requests
pip uninstall -y requests

pip list

pip list --outdated

pip freeze > requirements.txt

pip install -r requirements.txt

requirements.txt format

# Pin exact versions (common in production)
requests==2.28.0
flask==2.3.0
pandas==2.0.0

# Minimum version
numpy>=1.24.0

# Range
django>=4.0.0,<5.0.0

# Web stack
flask==2.3.0

# Data stack
pandas==2.0.0
numpy==1.24.0

# Dev-only (often a separate file)
# requirements-dev.txt
pytest==7.3.0
black==23.3.0
flake8==6.0.0

Advanced pip usage

pip install -e .
# Editable install: changes in source are visible without reinstall

pip install git+https://github.com/user/repo.git
pip install git+https://github.com/user/repo.git@branch_name

pip install package-1.0.0-py3-none-any.whl

pip install requests flask pandas

pip install --no-cache-dir requests

pip install --user requests

pip troubleshooting

pip install --upgrade pip

pip install --trusted-host pypi.org --trusted-host files.pythonhosted.org requests

pip install --user requests

pip install --force-reinstall requests

pip check

6. Virtual environments (basics)

Using venv

python -m venv myenv

# Windows
myenv\Scripts\activate

# macOS / Linux
source myenv/bin/activate

pip install requests

deactivate

7. Virtual environments (in depth)

How venv works

python -m venv myenv
# Common names: venv, .venv, env

# Layout:
# myenv/
# ├── Scripts/   # Windows
# ├── bin/       # Unix
# ├── Lib/
# └── pyvenv.cfg

# Windows PowerShell
myenv\Scripts\Activate.ps1

# Windows CMD
myenv\Scripts\activate.bat

# macOS / Linux
source myenv/bin/activate

# Prompt shows (myenv) when active

which python   # Unix
where python   # Windows
# Points at the interpreter inside the environment

pip install requests flask pandas
pip list
pip freeze > requirements.txt

deactivate

Typical workflow

mkdir myproject
cd myproject

python -m venv venv
venv\Scripts\activate  # Windows

pip install flask sqlalchemy pytest

pip freeze > requirements.txt

echo "venv/" >> .gitignore
git add .
git commit -m "Initial commit"

git clone <repo>
cd myproject
python -m venv venv
venv\Scripts\activate
pip install -r requirements.txt

Global vs virtual environment

Global installVirtual environment
LocationPython install dirProject directory
IsolationShared across projectsPer project
Version conflictsPossibleAvoided
Recommended forSystem toolsApplication work

8. Practical examples

Example 1: project skeleton

myproject/
├── venv/
├── src/
│   ├── __init__.py
│   ├── main.py
│   ├── config.py
│   ├── utils/
│   │   ├── __init__.py
│   │   ├── file_utils.py
│   │   └── string_utils.py
│   └── models/
│       ├── __init__.py
│       └── user.py
├── tests/
│   ├── __init__.py
│   ├── test_utils.py
│   └── test_models.py
├── docs/
│   └── README.md
├── .gitignore
├── requirements.txt
├── requirements-dev.txt
├── setup.py
└── README.md

Example 2: simple web scraper

web_scraper/
├── venv/
├── scraper/
│   ├── __init__.py
│   ├── main.py
│   ├── parser.py
│   └── storage.py
├── requirements.txt
└── README.md
# scraper/parser.py
import requests
from bs4 import BeautifulSoup

def fetch_page(url):
    """Download HTML for a URL."""
    response = requests.get(url, timeout=10)
    response.raise_for_status()
    return response.text

def parse_titles(html):
    """Extract heading text from HTML."""
    soup = BeautifulSoup(html, 'html.parser')
    titles = [h2.text.strip() for h2 in soup.find_all('h2')]
    return titles

# scraper/storage.py
import json
from pathlib import Path

def save_to_json(data, filename):
    """Write data to a JSON file."""
    with open(filename, 'w', encoding='utf-8') as f:
        json.dump(data, f, ensure_ascii=False, indent=2)

def load_from_json(filename):
    """Load JSON if the file exists."""
    if not Path(filename).exists():
        return None
    with open(filename, 'r', encoding='utf-8') as f:
        return json.load(f)

# scraper/__init__.py
"""
Web scraper package.
"""
from .parser import fetch_page, parse_titles
from .storage import save_to_json, load_from_json

__version__ = '1.0.0'
__all__ = ['fetch_page', 'parse_titles', 'save_to_json', 'load_from_json']

# scraper/main.py
from scraper import fetch_page, parse_titles, save_to_json

def main():
    url = 'https://example.com'
    
    print(f"Fetching: {url}")
    html = fetch_page(url)
    
    print("Parsing titles...")
    titles = parse_titles(html)
    
    save_to_json({'titles': titles}, 'output.json')
    
    print(f"Saved {len(titles)} titles")

if __name__ == '__main__':
    main()
requests==2.31.0
beautifulsoup4==4.12.0
python -m venv venv
venv\Scripts\activate
pip install -r requirements.txt
python -m scraper.main

9. Fixing import problems

Problem 1: ModuleNotFoundError

# ModuleNotFoundError: No module named 'mymodule'

# Cause 1: package not installed
# Fix: pip install mymodule

# Cause 2: path not on sys.path
import sys
print(sys.path)
sys.path.append('/path/to/module')

# Cause 3: wrong environment
import sys
print(sys.prefix)

Problem 2: Circular imports

# module_a.py
from module_b import func_b

def func_a():
    return func_b()

# module_b.py
from module_a import func_a  # circular!

# Fix 1: lazy import inside a function
def func_a():
    from module_b import func_b
    return func_b()

# Fix 2: shared module
# common.py with shared helpers

# Fix 3: refactor dependencies (best long-term)

Problem 3: Relative import errors

# ImportError: attempted relative import with no known parent package

# Cause: running a file inside a package directly
# python mypackage/module.py  # bad

# Fix: run as a module
# python -m mypackage.module

# Or use absolute imports:
# from mypackage.utils import helper

Problem 4: ImportError vs ModuleNotFoundError

# ModuleNotFoundError: module missing entirely

# ImportError: module exists but the name does not
# from math import nonexistent_function

import math
print(dir(math))

10. Practical tips

1) Import order (PEP 8 style)

# Standard library
import os
import sys
from datetime import datetime

# Third party
import requests
import numpy as np
from flask import Flask

# Local application
from mypackage import utils
from mypackage.models import User

2) Optional dependencies

try:
    import numpy as np
    HAS_NUMPY = True
except ImportError:
    HAS_NUMPY = False
    print("NumPy not installed; using pure Python fallback.")

def process_data(data):
    if HAS_NUMPY:
        return np.array(data).mean()
    return sum(data) / len(data)

3) Lazy imports

def process_image(image_path):
    from PIL import Image  # import only when needed
    
    img = Image.open(image_path)
    return img.resize((100, 100))

4) __all__

def public_func():
    """Public API."""
    pass

def _private_func():
    """Internal by convention (_ prefix)."""
    pass

__all__ = ['public_func']

# from mymodule import *  # only public_func

5) Reloading during development

import importlib
import mymodule

importlib.reload(mymodule)

# Avoid in production; debugging only

11. Exercises

Exercise 1: small utility package

Create this layout and use it:

utils/
├── __init__.py
├── math_utils.py  # add, multiply
└── text_utils.py    # reverse, count_words

Exercise 2: requirements.txt

Write requirements.txt with:

  • requests (2.31.0)
  • flask (>=2.3.0,<3.0.0)
  • pandas (latest)

Exercise 3: standard library

Use datetime and random to:

  • Print the date 7 days from today
  • Draw 10 unique random integers from 1–100
Answers
# utils/math_utils.py
def add(a, b):
    return a + b

def multiply(a, b):
    return a * b

# utils/text_utils.py
def reverse(text):
    return text[::-1]

def count_words(text):
    return len(text.split())

# utils/__init__.py
from .math_utils import add, multiply
from .text_utils import reverse, count_words

__all__ = ['add', 'multiply', 'reverse', 'count_words']

# main.py
from utils import add, reverse

print(add(3, 5))
print(reverse("Hello"))
requests==2.31.0
flask>=2.3.0,<3.0.0
pandas
from datetime import datetime, timedelta
import random

today = datetime.now()
after_7_days = today + timedelta(days=7)
print(after_7_days.strftime("%Y-%m-%d"))

random_numbers = random.sample(range(1, 101), k=10)
print(random_numbers)

Summary

Key takeaways

  1. Module: a .py file; import to reuse code.
  2. Package: directory of modules; __init__.py for regular packages.
  3. import styles: whole module, from-import, alias, star (avoid star).
  4. Standard library: os, datetime, random, json, sys, pathlib, …
  5. pip: install from PyPI; pin versions for reproducibility.
  6. Virtual environments: isolate dependencies per project (venv).
  7. requirements.txt: record dependencies for installs.

After you master the module system

  • Reuse code more confidently
  • Structure larger projects cleanly
  • Collaborate with clearer boundaries
  • Ship installs that match your environment

Next steps