Lab 06 — Build a Reusable Utility Package
Hands-on Lab 30 min Intermediate
Objectives
- Create a proper package directory structure
- Write multiple modules with public APIs
- Configure
__init__.pywith relative imports - Create and activate a virtual environment
- Write a
requirements.txt
Step 1 — Project Structure
mkdir ~/python-course/lab-06
cd ~/python-course/lab-06
# Create virtual environment
python3 -m venv venv
source venv/bin/activate # macOS/Linux
# venv\Scripts\Activate.ps1 # Windows
# Create package structure
mkdir -p pyutils/formatters
mkdir -p pyutils/validators
touch pyutils/__init__.py
touch pyutils/formatters/__init__.py
touch pyutils/validators/__init__.py
Step 2 — Create the Modules
pyutils/formatters/__init__.py:
from .text import truncate, title_case, slugify
from .numbers import format_currency, format_percentage, format_bytes
__all__ = ["truncate", "title_case", "slugify",
"format_currency", "format_percentage", "format_bytes"]
pyutils/formatters/text.py:
import re
def truncate(text: str, max_len: int = 50, suffix: str = "...") -> str:
"""Truncate text to max_len characters."""
if len(text) <= max_len:
return text
return text[:max_len - len(suffix)] + suffix
def title_case(text: str) -> str:
"""Convert to title case, handling articles and prepositions."""
SMALL_WORDS = {"a", "an", "the", "and", "but", "or", "in", "on", "at", "to"}
words = text.lower().split()
result = []
for i, word in enumerate(words):
if i == 0 or word not in SMALL_WORDS:
result.append(word.capitalize())
else:
result.append(word)
return " ".join(result)
def slugify(text: str) -> str:
"""Convert text to URL-friendly slug."""
text = text.lower().strip()
text = re.sub(r"[^\w\s-]", "", text)
return re.sub(r"[\s_-]+", "-", text).strip("-")
pyutils/formatters/numbers.py:
def format_currency(amount: float, symbol: str = "$", decimals: int = 2) -> str:
return f"{symbol}{amount:,.{decimals}f}"
def format_percentage(value: float, decimals: int = 1) -> str:
return f"{value:.{decimals}%}"
def format_bytes(num_bytes: int) -> str:
for unit in ["B", "KB", "MB", "GB", "TB"]:
if num_bytes < 1024:
return f"{num_bytes:.1f} {unit}"
num_bytes /= 1024
return f"{num_bytes:.1f} PB"
pyutils/validators/__init__.py:
from .validators import validate_email, validate_url, validate_phone
__all__ = ["validate_email", "validate_url", "validate_phone"]
pyutils/validators/validators.py:
import re
def validate_email(email: str) -> bool:
pattern = r"^[\w.+-]+@[\w-]+\.[a-zA-Z]{2,}$"
return bool(re.match(pattern, email))
def validate_url(url: str) -> bool:
pattern = r"^https?://[\w.-]+(?:/[\w./?=%&-]*)?$"
return bool(re.match(pattern, url))
def validate_phone(phone: str, country: str = "CA") -> bool:
patterns = {
"CA": r"^\+?1?[-.\s]?\(?\d{3}\)?[-.\s]?\d{3}[-.\s]?\d{4}$",
"FR": r"^(?:\+33|0)[1-9](?:\d{2}){4}$",
}
pattern = patterns.get(country, r"^\+\d{7,15}$")
return bool(re.match(pattern, phone.strip()))
pyutils/__init__.py:
from .formatters import (truncate, title_case, slugify,
format_currency, format_percentage, format_bytes)
from .validators import validate_email, validate_url, validate_phone
__version__ = "1.0.0"
__author__ = "Your Name"
Step 3 — Test It
Create demo.py at the project root:
import pyutils as pu
# Text formatting
print(pu.truncate("This is a very long text that needs truncating", max_len=30))
print(pu.title_case("the quick brown fox and the lazy dog"))
print(pu.slugify("Hello World! This is a Test 123"))
# Number formatting
print(pu.format_currency(1234567.89))
print(pu.format_percentage(0.8567))
print(pu.format_bytes(1_073_741_824))
# Validation
print(pu.validate_email("alice@example.com")) # True
print(pu.validate_email("not-an-email")) # False
print(pu.validate_url("https://inskillboost.com")) # True
python3 demo.py
Verification ✅
This is a very long text th...
The Quick Brown Fox and the Lazy Dog
hello-world-this-is-a-test-123
$1,234,567.89
85.7%
1.0 GB
True
False
True
Summary
- Created a multi-module package with sub-packages (
formatters,validators) - Each sub-package has its own
__init__.pyexposing a clean public API - Used relative imports (
from .text import ...) within the package - The top-level
__init__.pyre-exports everything for a flat import API