Article summary

TL;DR

  • Teach pytest basics, then move quickly to the team’s production code.
  • Pair developers in rotating weekly practice sessions.
  • Protect legacy systems at behavioral pinch points instead of rewriting everything.
  • Add tests to CI early and treat testability as part of system design.

01 / INTRODUCTION

Testing fails when it is introduced as an isolated skill.

Most Python teams do not struggle with testing because developers are lazy or pytest is hard. They struggle because testing is introduced as an isolated skill instead of a shared engineering practice.

Someone says, “We need more tests.” A few developers agree. Maybe a new tests/ folder appears. Maybe a pytest tutorial is shared in Slack. Maybe one person adds a GitHub Actions workflow. But the team still does not change its daily engineering habits.

That was the real problem I had to solve.

A few years ago, I was working as a team lead at an AI automotive startup in Berlin. The company was later acquired by Valeo and is now part of Seeing Machines. At that time, we had around 15 Python developers, a large legacy codebase, and a growing need to bring more of the system under test coverage (well, it was almost 0% coverage at the beginning to be fair). So, our team started weekly shared learning sessions around Brian Okken’s Python Testing with pytest, Second Edition and our own production codebase.

That experience eventually became the foundation for my course, Pytest Course: Python Test Automation & GitHub Actions CI/CD. But the course was not born from a simple idea like “pytest needs another video tutorial.”

It came from a more practical question:

How do you onboard an entire Python team when the code is already complex and delivery pressure is real?

02 / THE REAL PROBLEM

The real problem was not Pytest syntax.

Pytest itself is approachable. The official pytest getting-started guide shows how quickly a developer can write a test with plain assert, run pytest, and use automatic test discovery. Fixtures, parametrization, markers, mocking, coverage, and configuration can follow.

But team adoption is different from individual learning. In a real Python project, the difficult questions are usually:

  1. Where should we start testing if the codebase has almost no coverage?
  2. Which tests are worth writing first?
  3. When should we write unit tests, integration tests, or API tests?
  4. How do we avoid testing implementation details?
  5. How do we test code with legacy dependencies?
  6. How do we add tests without stopping feature delivery?
  7. How do we make testing part of implementation design instead of an afterthought?

This is why a simple pytest tutorial for Python developers is not enough. A team needs shared judgment, shared habits, and repeated practice.

03 / SHARED LEARNING

The format that actually worked.

The most useful part of our internal learning process was not the theory session alone. The key was the practice that followed it.

Each shared learning session had two parts:

  1. 30 to 60 minutes of theory. We covered the next chapter of Python Testing with pytest, discussed concepts, and translated them into examples from our codebase.

  2. Around 60 minutes of hands-on practice. Developers worked in pairs or small rotating groups. They wrote tests, refactored code, discussed design trade-offs, and applied Pytest to code that looked like our real work.

That second part changed everything. The practice sessions made testing social. Developers did not learn Pytest alone in a corner. They learned it by pairing, asking questions, reviewing assumptions, and seeing how another developer approached the same problem.

Rotating teams were especially important. One developer’s fixture pattern became another developer’s default. Someone who understood integration tests showed where a unit test would be too narrow. Knowledge moved across the team.

Testing culture spreads through repeated, practical, collaborative work.

For teams that lack testing expertise, this is crucial. Do not only schedule lectures. Schedule practice. Let people write tests together and struggle with real code together.

04 / BARRIERS

Practice exposed what was really blocking adoption.

Once we started writing tests together, two problems became very visible: perfectionism and legacy dependencies. Both can quietly block testing adoption.

Problem 1: perfectionism

A developer starts testing a module and quickly sees that the design is not ideal. Dependencies are mixed together. Functions are too large. The temptation is to pause everything and redesign the whole area before writing tests.

That sounds responsible, but it often becomes a trap. If every test requires a large refactor first, the team will conclude that testing is too expensive.

The pragmatic answer was the scout principle: leave the code better than you found it. We did not need to rewrite the whole system. We could make one small improvement, add one meaningful test, reduce one dependency, or clarify one boundary.

Problem 2: legacy dependencies

Some code had dependencies that were slow, unstable, global, hard to mock, or deeply coupled to infrastructure. If you try to test every class and method in that situation, you can waste time and create brittle tests.

The useful idea came from Michael Feathers’ Working Effectively with Legacy Code: pinch points, narrow places where many effects can be observed. Instead of testing every internal path, find a boundary where a small number of tests can protect a meaningful cluster of behavior.

  • Where can we detect the most important breakage with the fewest tests?

  • Which endpoint, service boundary, report builder, command, or workflow observes several important behaviors at once?

  • Can we write a characterization test around that point before changing internals?

This is more realistic than trying to reach perfect unit-test coverage immediately. Pytest can support characterization tests, integration tests, API tests, coverage gates, and later smaller unit tests as the design improves.

05 / MAIN LEARNING

Testing strategy belongs inside system design.

The biggest lesson from the process was simple: testing strategy should be part of system design and implementation design. It should not be bolted on at the end.

When designing a feature, a team should ask:

  1. How will we know this works?
  2. Which behavior should be protected by unit tests?
  3. Which behavior needs integration tests?
  4. What should run in CI on every pull request?
  5. Which dependencies should be real, mocked, or replaced with test doubles?
  6. What design would make this feature easier to test without over-engineering it?

This is where TDD and BDD are useful, even if you do not follow them literally all the time. TDD teaches feedback loops, behavior, and small increments. BDD helps describe expected behavior in a way that connects technical implementation with user-facing outcomes.

The long-term speed comes from combining these ideas pragmatically: understand the tools, avoid giant rewrites when a small improvement will do, make tests part of design, and run the right tests automatically in CI.

Teams go faster because the system becomes safer to change.

06 / THE COURSE

Why I turned this into a course.

After that first team experience, I joined another Berlin AI startup and saw the same pattern again: good Python developers doing useful product work, but uneven testing habits and not enough confidence around software testing and automation workflows.

That repetition convinced me the problem was not unique to one company. Developers need a learning path that connects Pytest fundamentals, fixtures, parametrization, mocking, integration testing, API testing, FastAPI, coverage, the testing pyramid, TDD/BDD, GitHub Actions, and practical work with legacy code.

The course is intentionally practical. It is not only a list of Pytest features. The examples include testing a scikit-style feature, a document editor, FastAPI endpoints, temporary files and logs, monkeypatching, finance-tracker challenges, coverage gates, matrix testing, and parallel CI workflows.

Developers do not only need syntax. They need to use testing to design, refactor, debug, and ship Python code with more confidence.

Start the Pytest course right here and try the first lessons free.

View course

07 / TODAY

How I would onboard a Python team today.

I would treat onboarding as a small, repeatable delivery practice rather than a separate training project.

  1. Week 1 - learn the essentials. Use a short pytest tutorial to cover test discovery, assertions, fixtures, parametrization, and local runs.
  2. Week 1 - apply them immediately. After the first hour, pair on one important behavior in the team’s real codebase.
  3. Weeks 2 to 4 - rotate the practice. Run weekly sessions in changing pairs or small groups so patterns and judgment move across the team.
  4. Start with legacy pinch points. Protect an endpoint, service boundary, report, command, or workflow before testing every class underneath it.
  5. Add CI early. Use GitHub’s Python build-and-test workflow guidance to run the useful test suite on every pull request, so failures become part of the review conversation.
  6. Keep testing inside feature design. Discuss test level, boundaries, dependencies, and feedback speed before implementation is finished.

The timing is a starting point, not a rigid calendar. Adjust it to the codebase and delivery pressure, but keep the tone pragmatic: no guilt, no purity contest, and no giant rewrite.

Write the next useful test. Improve the next small piece of design. Leave the code better than you found it.

08 / FINAL THOUGHT

Testing is a team habit and a design habit.

The story behind my pytest course is not only about learning a testing framework. It is about making Python testing part of how a team designs, changes, and ships software, even when the codebase is old and delivery pressure is real.

That is why the course goes beyond pytest syntax. It connects unit tests, integration tests, test automation, refactoring, and CI to the decisions developers make in real projects.

If your team is introducing Python test automation, start smaller than you think. Start with shared practice. Start with real code. Start with one useful test near an important behavior.

Then keep going.