Python
Completed
-
Section 1: Getting Started
-
Section 2: Core Syntax and Data Types
-
Section 3: Collections
-
39: Set Operations: Union, Intersection, Difference
-
Section 4: Control Flow
-
Section 5: Functions
-
Section 6: Turtle Graphics and Early Practice Projects
-
Section 7: Working with Files and I/O
-
Section 8: Regular Expressions
-
Section 9: Object-Oriented Python
-
Section 10: Error Handling
-
Section 11: Modules and Packages
-
Section 12: Iterators, Generators, and Functional Tools
-
Section 13: Decorators and Metaprogramming
-
Section 14: Concurrency and Parallelism
-
Section 15: Working with Dates, Times, and Numbers
-
Section 16: Standard Library Deep Dive I: Data Structures
-
Section 17: Standard Library Deep Dive II: System and Introspection
-
Section 18: Standard Library Deep Dive III: Security and Encoding
-
Section 19: Standard Library Deep Dive IV: Text and Data Utilities
-
Section 20: Networking and Web Basics
-
Section 21: Working with Databases
-
Section 22: Testing and Quality
-
Section 23: Advanced Typing
-
Section 24: Context Managers and Resource Handling
-
Section 25: Text, Unicode, and Binary Data
-
Section 26: More Functional and Iteration Tools
-
Section 27: Data Validation and Configuration
-
Section 28: Working with Images and Media
-
Section 29: Property-Based and Documentation Testing
-
Section 30: Packaging and Deployment
-
Section 31: Performance and Internals
-
Section 32: Design Patterns in Python
-
Section 33: GUI Programming
-
Section 34: Security Basics
-
Section 35: Data Structures and Algorithms
-
Section 36: Practical Projects
-
Section 37: Capstone Projects
-
Section 38: Interview and Algorithm Practice
-
Section 39: Writing Idiomatic Python
341: Writing and Running Doctests
I've seen this happen to dozens of developers. You write a handy little utility function, you add a few examples in the docstring to show how it works, and then you run the doctest module only to be met with a wall of failure. You look at the code, look at the expected output, and they look identical. It's infuriating.
def trim_and_capitalize(text):
"""
Removes leading/trailing whitespace and capitalizes the first letter.
>>> trim_and_capitalize(" hello world ")
'Hello world'
"""
return text.strip().capitalize()
if __name__ == "__main__":
import doctest
doctest.testmod()
If you run this, Python will tell you the test failed. It will show you that it expected 'Hello world' but got 'Hello world'. If you look closely at the code above, I've hidden a trailing space after the closing quote in the docstring's expected output. To the naked eye, it looks fine. To doctest, it's a total mismatch.
The Invisible Character Trap
The core issue here is that doctest isn't just checking if your function works; it's performing a literal string comparison between what your function returns (represented as a string) and what you typed in the docstring. It's incredibly pedantic. A single misplaced space, a missing newline, or even a difference in how a list is represented (single quotes vs. double quotes) will trigger a failure.
When you see a failure, don't just stare at the code. Look at the "Diff" provided by the output. Python will usually show you a + and a - line. The - is what the function actually produced, and the + is what you told the test to expect. If they look the same, highlight the text with your mouse to see if there are trailing spaces hiding at the end of the line.
Aligning Expectations with Reality
To fix the bug, we have to ensure the docstring is a mirror image of the Python REPL output. I usually recommend writing the function first, running it manually in a terminal, and then copy-pasting the result directly into the docstring. That way, you aren't guessing how Python formats the output.
def trim_and_capitalize(text):
"""
Removes leading/trailing whitespace and capitalizes the first letter.
>>> trim_and_capitalize(" hello world ")
'Hello world'
"""
return text.strip().capitalize()
Now, the trailing space is gone. The test passes. It seems trivial, but this "exact match" requirement is why many people give up on doctests. The trick is to treat your docstrings as code, not just comments.
Running Tests Without Boilerplate
In the first example, I added the if __name__ == "__main__": block. That's fine for a quick script, but in a professional project, I don't want test-execution logic cluttering up my production modules. You can run doctests directly from your shell instead.
Try running this in your terminal:
python -m doctest my_script.py
By default, this command is silent if everything passes. If you want to see the detailed results (the "verbose" mode), add the -v flag:
python -m doctest -v my_script.py
I prefer the verbose flag during development because it gives me a satisfying confirmation that every single example was actually executed and passed, rather than just hoping for the best because there were no errors.
Choosing Doctests Over Unit Tests
You might be wondering why you'd bother with this when we have unittest or pytest. Here is my rule of thumb: use doctests for documentation and unit tests for verification.
- Doctests are great for "happy path" examples. They tell the user, "If you give the function X, you will get Y." They serve as live documentation that can't go out of date.
- Unit Tests are for the "sad paths." Edge cases, error handling, and complex logic belong in a separate test file where you have more powerful tools than simple string comparison.
If you find yourself writing 20 different doctests for one function, you're using the wrong tool. Move the edge cases to a test suite and keep the docstring clean and illustrative.
📋 Practical Task
Building a Validated Email Domain Extractor
Your task is to create a utility function called extract_domain that takes an email address string and returns only the domain part (everything after the '@' symbol). However, the function should return None if the email is invalid (i.e., it doesn't contain exactly one '@' symbol).
To complete this exercise, you must:
- Implement the
extract_domainlogic. - Write three doctests within the function's docstring:
- One for a standard email (e.g.,
"user@example.com"should return'example.com'). - One for an email with no '@' symbol (should return
None). - One for an invalid email with multiple '@' symbols (should return
None).
- One for a standard email (e.g.,
- Ensure the doctests pass exactly when run with the
doctestmodule.
Note: Pay close attention to the representation of None in your docstring; it should not be wrapped in quotes.
There are no comments for now.