DEV Community

Cover image for My Machine Learning Project Taught Me More About Software Engineering Than Machine Learning
Michael Nyawade
Michael Nyawade

Posted on

My Machine Learning Project Taught Me More About Software Engineering Than Machine Learning

When I started building my ad click prediction project, I thought the hard part would be training a machine learning model.

I was wrong.

Before I even got to preprocessing the data, I found myself dealing with project architecture, testing, imports, packaging, and debugging environment issues. Looking back, those lessons were probably more valuable than the model itself.

Here's what happened.

It Started With a Notebook

Like many data science projects, everything began inside a Jupyter notebook.

Loading the dataset was as simple as:

df = pd.read_csv("data/raw/Advertising.csv")
Enter fullscreen mode Exit fullscreen mode

It worked perfectly.

At first glance, there wasn't much reason to change it. The dataset loaded correctly, there were no missing values, no duplicates, and I could immediately start exploring the data.

But there was one problem.

The notebook knew how to load the data.

Nothing else did.

Separating Exploration From Reusable Code

Instead of leaving everything inside the notebook, I moved the loading logic into a dedicated module.

src/
└── data/
    └── load_data.py
Enter fullscreen mode Exit fullscreen mode

The notebook changed from doing the work itself to simply calling a function:

from src.data.load_data import load_raw_data

df = load_raw_data("data/raw/Advertising.csv")
Enter fullscreen mode Exit fullscreen mode

It doesn't feel like a huge improvement until you think about the future.

Now every notebook, script, or training pipeline can reuse the same code instead of copying and pasting it.

Small change.

Big difference.

Defensive Programming Matters

The next improvement wasn't about functionality.

It was about failure.

Instead of relying on pandas.read_csv() to throw an error if the dataset disappeared, I added an explicit check that raises a meaningful FileNotFoundError.

That seemed unnecessary at first.

But then I realized something.

Good software doesn't just work when everything is correct.

It also fails in ways that are easy to understand.

A descriptive error today can save a lot of debugging tomorrow.

Writing My First Test

The next step was introducing automated testing.

The first test was intentionally simple.

It verified that:

  • the dataset loads successfully,
  • it has the expected number of rows and columns,
  • the target column exists.

Nothing fancy.

Still, it was the first time the project had a safety net.

Or so I thought.

The Test Didn't Fail Because of My Code

Instead, it failed with this:

ModuleNotFoundError: No module named 'src'
Enter fullscreen mode Exit fullscreen mode

At first I assumed I had made a mistake.

I hadn't.

The notebook worked because I had manually modified sys.path.

pytest, however, starts a fresh Python process.

It had no idea where src lived.

The interesting part wasn't the error.

It was what the error revealed.

My project structure wasn't as solid as I thought.

The "Best Practice" Didn't Work

The first idea was to package the project properly using pyproject.toml and install it in editable mode.

That's the modern Python approach and, in many environments, it's absolutely the right solution.

Except...

My development environment had other ideas.

Running:

python -m pip install -e .
Enter fullscreen mode Exit fullscreen mode

didn't work because of the packaging tools available in the hosted Anaconda environment.

For a moment, it felt like I'd gone down the wrong path.

But that turned into another lesson.

Sometimes the technically "best" solution isn't the practical one for your environment.

Pragmatism Beats Perfection

Instead of spending hours fighting the environment, I switched to a simpler approach.

Adding a small tests/conftest.py file allowed pytest to locate the project correctly.

One command later:

pytest
Enter fullscreen mode Exit fullscreen mode

The result:

1 passed
Enter fullscreen mode Exit fullscreen mode

Success.

More importantly, I learned something that applies far beyond Python.

Engineering isn't about blindly following best practices.

It's about choosing the right level of complexity for the problem you're solving.

The Real Milestone

By the end of this stage, I hadn't trained a model.

I hadn't engineered features.

I hadn't even started preprocessing.

Yet the project had already changed dramatically.

Instead of a single notebook, I now had:

  • a reusable data loading module,
  • a notebook that consumes reusable code instead of owning it,
  • automated tests,
  • a working pytest configuration,
  • clearer error handling,
  • a cleaner project structure.

That feels much closer to a production codebase than a collection of experiments.

What I'll Do Next

The next stage is building a reusable preprocessing pipeline.

Rather than cleaning data directly inside the notebook, I'll move that logic into src/data/preprocess.py, where every transformation can be tested and reused.

The notebook will become a client of the pipeline instead of the pipeline itself.

I'm beginning to appreciate that building a machine learning project isn't just about choosing algorithms.

It's about building software that happens to use machine learning.

And honestly, that's turning out to be the most valuable lesson so far.

Top comments (0)