When Breakpoints Don't Break: A pytest-cov Debugging Mystery

written by Chris Goodwin on 12/23/2022

Quick Solution

If your breakpoints aren't working and you use pytest-cov, run your tests with:

pytest your_tests.py --no-cov

The Problem

You're debugging a Django application with pytest, and your breakpoints aren't stopping where they should. If this is happening to you, you're not alone - there's a known issue between coverage, pytest-cov, and factory_boy that can cause breakpoints to behave unexpectedly.

Environment and Versions

This issue affects:

  • coverage versions 6.4.1 and later
  • pytest-cov (all recent versions)
  • Python 3.x+
  • Django 3.x+

A Real-World Example

Let's walk through a typical scenario where this issue appears. Consider a simple Django user model with a full_name property:

class User(AbstractUser):
    first_name = models.CharField(max_length=30)
    last_name = models.CharField(max_length=30)

    @property
    def full_name(self):
        return f"{self.first_name} {self.last_name}"

Using Factory Boy (a fantastic tool for test data generation), we've created a factory:

class UserFactory(factory.django.DjangoModelFactory):
    first_name = factory.Faker("first_name")
    last_name = factory.Faker("last_name")
    username = factory.LazyAttributeSequence(
        lambda a, n: f"{a.first_name}{a.last_name}{n}".lower()
    )

    class Meta:
        model = User

Now, let's write a simple test:

class TestUser(TestCase):
    def test_full_name_method(self):
        """Test the full_name property returns the expected value"""
        user = UserFactory.create(
            first_name="Bob",
            last_name="Roberts"
        )
        breakpoint()  # This should stop here, but might not!
        self.assertEqual(user.full_name, "Bob Roberts")

The Unexpected Behavior

When you run this test with pytest-cov enabled, something strange happens:

  1. Expected: The debugger should stop at the breakpoint() line in your test
  2. Actual: The debugger jumps to the full_name property definition instead

Here's what you might see:

--Call--
> /path/to/your/app/models.py(XX)full_name()
-> @property
(Pdb) print(user)
*** NameError: name 'user' is not defined

Solutions and Workarounds

1. Disable Coverage During Debugging

The simplest solution is to disable coverage when you need to use breakpoints:

# Option 1: Disable coverage completely
pytest your_tests.py --no-cov

# Option 2: Run specific test with no coverage
pytest your_tests.py::TestUser::test_full_name_method --no-cov

2. PyCharm Configuration

If you're using PyCharm, you can permanently configure this in your debug settings:

  1. Go to Run/Debug Configurations
  2. Add --no-cov to the "Additional Arguments" field
  3. Save the configuration

PyCharm Debug Configuration

3. Alternative Debugging Methods

It is worth noting that you could downgrade coverage far enough that the issue goes away - but that is not a practical solution.

If you need to maintain coverage while debugging:

def test_full_name_method(self):
    user = UserFactory.create(first_name="Bob", last_name="Roberts")

    # Option 1: Use print statements
    print(f"Debug: user object = {user}")
    print(f"Debug: full name = {user.full_name}")

    # Option 2: Use logging
    import logging
    logging.debug(f"User object: {user}")

    self.assertEqual(user.full_name, "Bob Roberts")

Impact on CI/CD

This issue typically only affects local development since you generally don't use breakpoints in CI/CD pipelines. Your coverage reports in CI/CD should continue to work normally.

Root Cause

The issue stems from how coverage instrumentation interacts with Python's debugging hooks. When coverage is enabled:

  1. Coverage instruments your code to track execution
  2. This instrumentation can interfere with Python's standard debugging hooks
  3. The result is that breakpoints may fire at unexpected locations

Related Issues and Discussion

Further Reading

While this issue can be frustrating, the --no-cov workaround provides a reliable solution. Until the underlying interaction between coverage and debugging is resolved (which may never happen), this remains the most practical approach for local development debugging.

Remember:

  • Use --no-cov when you need breakpoint debugging
  • Consider alternative debugging methods if you must maintain coverage
  • Keep your CI/CD pipeline configurations unchanged

Happy debugging!

Did you find this article helpful?

Feel free to help me keep this blog going (with coffee)!