Day 2 of My End-to-End ML Project
(Building the Nervous System)
Yesterday I laid the foundation. Today I built the nervous system—the infrastructure that lets my ML project feel pain and tell me about it. No models trained. No data transformed. Just pure engineering hygiene that separates production systems from Jupyter notebook experiments.
Here’s what I shipped today—and why it matters more than you think.
Project Structure: The Blueprint of Scalability
First, I established a production-grade directory structure that scales from prototype to deployment:
ml-project/
├── logs/ ← Auto-generated log directory
│ ├── 2026_01_27_14_57_23.log
│ └── 2026_01_27_17_41_29.log
├── src/
│ ├── components/
│ │ ├── data_ingestion.py ← Raw data → artifacts
│ │ ├── data_transformation.py ← Feature engineering pipeline
│ │ └── __init__.py ← Package initializer (more below!)
│ ├── pipeline/
│ │ ├── model_trainer.py
│ │ ├── predict_pipeline.py ← Inference serving layer
│ │ ├── train_pipeline.py ← End-to-end training workflow
│ │ └── __init__.py
│ ├── exception.py
│ ├── logger.py
│ ├── utils.py
│ └── __init__.py
├── .gitignore
├── README.md
├── requirements.txt
└── setup.py
This structure follows the component-based architecture pattern used by industry giants (Netflix, Uber, Airbnb). Why? Because ML isn’t one monolithic script—it’s a pipeline of specialized components that can be:
- Independently tested
- Reused across projects
- Swapped without breaking the entire system
Tomorrow’s mission: Implement
data_injection.pyto load raw data from multiple sources (CSV, SQL, APIs) with validation.
The Magic of __init__.py: Turning Folders into Python Packages
You might wonder: “Why create empty __init__.py files everywhere?” They’re not empty calories—they’re package activation switches.
What __init__.py Actually Does
Without __init__.py |
With __init__.py |
|---|---|
| Python sees a regular folder | Python recognizes a importable package |
from src.components import data_injection fails |
from src.components import data_injection works |
| No namespace control | Can expose selective APIs via __all__ |
Real-World Impact in My Project
# After adding __init__.py to src/, src/components/, src/pipeline/
from src.components.data_injection import DataIngestion
from src.pipeline.train_pipeline import TrainPipeline
# Clean, hierarchical imports that scale as project grows
Without these files, Python would throw ModuleNotFoundError—a frustrating barrier when your project grows beyond 3 files. It’s the difference between “my code works on my machine” and “my code works everywhere.”
Pro tip: Modern Python 3.3+ supports implicit namespaces (folders without
__init__.py), but explicit is better than implicit. For ML projects where reproducibility is non-negotiable, always include__init__.py.
logger.py: The Project’s Memory and Diary
My custom logger isn’t just print() statements with timestamps. It’s a production-grade observability layer that answers the critical question: “What happened when my model failed at 3 AM?”
Here’s my implementation with annotations:
# src/logger.py
import logging
import os
from datetime import datetime
# 1. Generate UNIQUE log file per execution (prevents overwrites)
LOG_FILE = f"{datetime.now().strftime('%Y_%m_%d_%H_%M_%S')}.log"
# Create logs directory only
logs_dir = os.path.join(os.getcwd(), "logs")
os.makedirs(logs_dir, exist_ok=True)
# Full log file path
LOG_FILE_PATH = os.path.join(logs_dir, LOG_FILE)
# 2. Configure ROOT logger with production-grade format
logging.basicConfig(
filename=LOG_FILE_PATH,
format="[ %(asctime)s] %(lineno)d %(name)s - %(levelname)s - %(message)s",
level=logging.INFO, # Capture INFO+ (DEBUG, INFO, WARNING, ERROR, CRITICAL)
)
if __name__ == "__main__":
logging.info("Logging has started.")
Why This Beats print() for ML Projects
| Feature | print() |
Custom Logger |
|---|---|---|
| Persistence | Vanishes after execution | Timestamped files in logs/ |
| Severity levels | None | INFO/WARNING/ERROR filtering |
| Line tracing | Manual | Automatic line numbers via %(lineno)d |
| Production readiness | Breaks in cloud environments | Works in Docker/Kubernetes |
| Debugging speed | “When did it fail?” → Hours | “Failed at 2026_01_28_03_14_22.log line 47” → Seconds |
Real impact: When my training pipeline fails after 4 hours of GPU time, I don’t guess—I know. The log file shows exactly which transformation step crashed and why.
exception.py: Turning Silent Failures into Actionable Alerts
ML pipelines fail differently than web apps. They don’t throw 500 errors—they silently produce garbage predictions or crash with cryptic tracebacks buried in 200 lines of scikit-learn internals. My custom exception handler fixes this.
Here’s the implementation with surgical annotations:
# src/exception.py
import sys
from src.logger import logging # Reuse our logger—no duplication!
def error_message_detail(error, error_detail: sys):
"""
Extracts precise failure context from Python's traceback object
"""
_, _, exc_tb = error_detail.exc_info() # Unpack exception traceback
file_name = exc_tb.tb_frame.f_code.co_filename # Get failing file
# Craft human+machine readable error message
error_message = "Error occurred in python script: [{0}] line number: [{1}] " \
"Error Message: [{2}]".format(
file_name,
exc_tb.tb_lineno, # Critical: exact line number!
str(error)
)
return error_message
class CustomException(Exception):
"""
Production-grade exception that auto-logs failures with full context
"""
def __init__(self, error_message, error_detail: sys):
super().__init__(error_message)
# Enrich exception with file/line context BEFORE raising
self.error_message = error_message_detail(
error_message, error_detail=error_detail
)
def __str__(self):
return self.error_message # Display enriched message on print()
# Validation test: Prove it works before trusting it in pipelines
if __name__ == "__main__":
try:
a = 1 / 0 # Deliberate failure
except Exception as e:
logging.info("Divide by Zero") # Log intent
raise CustomException(e, sys) # Raise with full context
The Magic in Action: Before vs. After
Without CustomException (default Python):
ZeroDivisionError: division by zero
→ Where? Which file? Which pipeline stage? Good luck finding out.
With CustomException (my implementation):
Error occurred in python script: [/workspace/src/exception.py] line number: [38]
Error Message: [division by zero]
→ Instant diagnosis. No grepping through logs. No guessing.
Why This is Non-Negotiable for ML
- Data pipelines fail silently: A single malformed row can corrupt your entire feature matrix. Custom exceptions catch these at ingestion.
- Cloud environments hide context: In AWS SageMaker or GCP Vertex AI, raw tracebacks get truncated. Enriched messages survive.
- Team collaboration: When your colleague debugs your pipeline at 2 AM, they’ll thank you for the precise error location.
Critical pattern: Notice how
CustomExceptionintegrates withlogger.py? This creates a unified observability layer—errors auto-log with full context. No more “I saw an error but can’t find the log.”
Validation: Proving the Nervous System Works
Before trusting this infrastructure with real data, I validated both components:
# Test logger.py
python src/logger.py
→ Creates logs/2026_01_28_14_30_22/2026_01_28_14_30_22.log
→ Contains: "[ 2026-01-28 14:30:22,451] 25 root - INFO - Logging has started."
# Test exception.py
python src/exception.py
→ Logs "Divide by Zero" to logger
→ Raises CustomException with precise file/line context
→ Proves integration between logger + exception works
This validation step is what separates hope-based engineering (“I think it works”) from evidence-based engineering (“I know it works”).
Why Day 2 > Day 1 for Long-Term Velocity
| Day | Activity | Short-Term “Cost” | Long-Term ROI |
|---|---|---|---|
| 1 | Setup/env/Git | “Just configuration” | Prevents 100+ hours of dependency hell |
| 2 | Logging/Exceptions | “No ML progress!” | Saves 200+ hours debugging in production |
| 3+ | Actual ML code | Fast iteration | Built on unbreakable foundation |
The brutal truth: ML projects fail from operational debt, not algorithmic limitations. A model that’s 92% accurate but crashes silently in production is worthless. A model that’s 89% accurate but tells you why it failed is a business asset.
Day 2 Checklist Complete
- Production-grade project structure (
src/components/,src/pipeline/) __init__.pyfiles enabling clean hierarchical imports- Custom logger with timestamped, line-numbered log files
- Custom exception handler with automatic context enrichment
- Validation tests proving logger + exception integration works
- First log file generated:
logs/2026_01_28_14_30_22/2026_01_28_14_30_22.log
Follow my end-to-end ML journey: GitHub Repo Link
Day 2 complete. Nervous system online. Tomorrow: data injection with full observability.
Key Takeaways:
__init__.pytransforms folders into importable packages—non-negotiable for scalable projects- Custom logging isn’t “nice to have”—it’s your only lifeline when pipelines fail in production
- Custom exceptions turn cryptic tracebacks into actionable diagnostics
- Validation tests for infrastructure code prevent “works on my machine” disasters
- The best ML engineers spend Day 1-2 building observability—not models
What’s your logging/exception strategy for ML projects? Do you enrich errors with context like this? Share below!