Skip to main content
Test Double company logo
Services
Services Overview
Holistic software investment consulting
Software Delivery
Accelerate quality software development
Product Impact
Drive results that matter
Legacy Modernization
Renovate legacy software systems
Pragmatic AI
Solve business problems without hype
Upgrade Rails
Update Rails versions seamlessly
DevOps
Scale infrastructure smoothly
Technical Recruitment
Build tech & product teams
Technical & Product Assessments
Uncover root causes & improvements
Case Studies
Solutions
Accelerate Quality Software
Software Delivery, DevOps, & Product Delivery
Maximize Software Investments
Product Performance, Product Scaling, & Technical Assessments
Future-Proof Innovative Software
Legacy Modernization, Product Transformation, Upgrade Rails, Technical Recruitment
About
About
What's a test double?
Approach
Meeting you where you are
Founder's Story
The origin of our mission
Culture
Culture & Careers
Double Agents decoded
Great Causes
Great code for great causes
EDI
Equity, diversity & inclusion
Insights
All Insights
Hot takes and tips for all things software
Leadership
Bold opinions and insights for tech leaders
Developer
Essential coding tutorials and tools
Product Manager
Practical advice for real-world challenges
Say Hello
Test Double logo
Menu
Services
BackGrid of dots icon
Services Overview
Holistic software investment consulting
Software Delivery
Accelerate quality software development
Product Impact
Drive results that matter
Legacy Modernization
Renovate legacy software systems
Pragmatic AI
Solve business problems without hype
Cycle icon
DevOps
Scale infrastructure smoothly
Upgrade Rails
Update Rails versions seamlessly
Technical Recruitment
Build tech &Ā product teams
Technical & Product Assessments
Uncover root causes &Ā improvements
Case Studies
Solutions
Solutions
Accelerate Quality Software
Software Delivery, DevOps, & Product Delivery
Maximize Software Investments
Product Performance, Product Scaling, & Technical Assessments
Future-Proof Innovative Software
Legacy Modernization, Product Transformation, Upgrade Rails, Technical Recruitment
About
About
About
What's a test double?
Approach
Meeting you where you are
Founder's Story
The origin of our mission
Culture
Culture
Culture & Careers
Double Agents decoded
Great Causes
Great code for great causes
EDI
Equity, diversity & inclusion
Insights
Insights
All Insights
Hot takes and tips for all things software
Leadership
Bold opinions and insights for tech leaders
Developer
Essential coding tutorials and tools
Product Manager
Practical advice for real-world challenges
Say hello
Developers
Developers
Developers
Future-proof innovative software

šŸ Functionally Zen

Functional programming primitives can steer a Python codebase toward simplicity. Pure functions, immutable data, and composition reduce complexity and keep side effects where they belong.
Kyle Adams
|
April 6, 2026
Thank you! Your submission has been received!
Oops! Something went wrong while submitting the form.
ā€œKeep it simple, stupid.ā€

The KISS principle was coined by Kelly Johnson, lead engineer at Lockheed Skunk Works. It is echoed in the third tenet of the Zen of Python:

ā€œSimple is better than complex.ā€

In that same spirit, I propose these additional tenets in our pursuit of Python coding simplicity:

  1. Idiomatic code is simpler than non-idiomatic code.
  2. Data is simpler than functions.
  3. Pure functions are simpler than impure functions.
  4. Pure functions are simpler than classes.
  5. Impure functions and/or classes are inescapable, so let’s minimize them.
  6. Constructors without side effects are simpler than those with side effects.
  7. Tests with no mocking are simpler than tests with mocking.

Finally: these ideas are generally true. There are likely specific cases where they are not true. Special cases aren’t special enough to break the rules.

The terminology

Specificity is the soul of narrative.

Here are some definitions to be more clear and precise as we discuss the rationale for these tenets:

Idiomatic: particular to or emblematic of a particular language

Side effects: interactions with external systems, such as the file system, a database, a user (via an interface), or a third-party API

Immutable data structure: a data structure that, once set, never changes

  • If changes are needed, they are made by replacing that data structure with a new, updated one
  • Python’s tuple, frozenset, and the upcoming frozendict (in Python 3.15) are examples of immutable data structures

Pure function: a function that, given a set of inputs, always produces the same outputs, free of side effects

State: data, but in this case specifically data bound to an instance of a class, which is initialized by invoking the class’s constructor

Methods: a special type of function that is bound to a class and its state

Instance methods: methods that are bound to a constructed/initialized instance of a class; they have access to the instance’s initialized state

Class methods: methods that are bound directly to the class, and thus do not have access to an initialized state

Functional programming: a way of organizing and thinking about code with functions

  • Though some may see it in opposition to object-oriented programming (OOP), the two ways of organizing code can co-exist
  • For example, you can write a class whose methods are pure functions and whose state consists of immutable data structures

Functional programming primitives: these are the core concepts that power functional programming, including pure functions and immutable data structures

  • Other examples: map(), filter(), and reduce()

Mocking: used here in a general sense to cover all the various testing methods—spies, test doubles, stubs, etc.—of controlling and/or inspecting dependencies outside the system under test

The tenets

Idiomatic code is simpler than non-idiomatic code

All programming languages have idioms that arise from their particular syntax, design goals, standard libraries, etc. Sometimes those idioms can conflict or at least be in tension. For example, many functional (or aspirationally functional) languages have adopted the map/reduce idiom:

> [1, 2, 3].map(x => x * 2)
[2, 4, 6]

Python took a different approach, as its creator, Guido van Rossum, once stated:

ā€œI value readability and usefulness for real code. There are some places where map() and filter() make sense, and for other places Python has list comprehensions.ā€

Consequently, the idiomatic approach for a simple mapping (transforming one element to another) in Python is not `map()` but rather a list comprehension:

>>> [x * 2 for x in [1, 2, 3]]
[2, 4, 6]

Though map() is more typical for other languages, list comprehensions will be more familiar to Python developers, particularly those who have not been exposed to other implementations of map().

Data is simpler than functions

Data is the virtual equivalent of ā€œno moving partsā€. No moving parts is intrinsically simpler than something—that is, a function—which does have moving parts. This tenet is particularly true when the data in question is stored in an immutable data structure.

I once worked on a medical app that took in parameters—age, weight, and lab results—and returned the proper dosage. The dosage calculation came from a medical textbook and was moderately complex. My initial thought was to implement the calculation as a function; however, my wiser coworker realized that the textbook also had lookup tables.

When the textbook had been written, dosages were calculated manually, so these lookup tables saved physicians from the error-prone work of doing their own calculations. The textbook converted what would have been a function into a data structure. We implemented those lookup tables as a three dimensional dictionary and this:

>>> calculate_dose(age, weight, lab_measurement)

Became this:

>>> lookup_dose[age][weight][lab_measurement]

As the dictionary didn’t have any logic that we’d implemented, we didn’t need to unit test anything.

Pure functions are simpler than impure functions

Since pure functions are consistent in the value they return, we never have the cognitive overload of considering how external systems might change how the function works. For example:

>>> def double(x):
...     return x * 2
...
>>> double(2)
4

We know that invoking double(2) will always result in 4 being returned. On the other hand:

>>> import requests
>>> def fetch_double(x):
...     resp = requests.post('https://example.com/doubler', data={'x': x})
...     body = resp.json()
...     return body["answer"]
...

We know that invoking fetch_double(2) will not always return 4. Sometimes it might. Sometimes it might return a 500 HTTP error. Sometimes it may raise a JSONDecodeError. If a developer makes a mistake, the call may even return 3.14! Consequently, pure functions are simpler than impure functions. Furthermore…

Pure functions are simpler than classes

The whole point of classes is to bundle together the state and the behavior that operates on that state. Consequently, a class is by definition more complex than a pure function, which is only behavior. Pure functions do not require the cognitive overhead of tracking how the state has changed, in addition to reading through the behavior. In this somewhat contrived example, compare this:

>>> def double(x):
...     return x * 2
...

With this:

>>> class Doubler:
...     def __init__(self, x):
...         self.x = x
...     def double():
...         return self.x * 2
...

The class requires a lot of boilerplate code to scaffold out that state, complexity that the pure function form does not need. Furthermore, any developer reading the code will need to remember what x was set to at initialization in order to reason about what a call to `double()` will return.

Impure functions and/or classes are inescapable, so let’s minimize them

At some point, we’ll need to interact with things, be they users, a database, or other APIs. This fundamental truth underlies the idea of ā€œfunctional core, imperative shellā€, as outlined by Gary Bernhardt in his Boundaries talk. (You can read ā€œimperativeā€ here to mean ā€œside effectsā€.) We should try to keep more complex, impure functions a thin layer around simpler pure functions, thereby keeping the overall system as simple as possible. Note that an important helper here is the Gang of Four’s assertion:

ā€œFavor object composition over class inheritance.ā€

Composing functions lets us separate side effects from the logic that needs to happen to them. We see this principle within Python’s standard library: json.load() could have taken in a file path as a string and opened it, but that would have mixed opening a file (side effect) with marshaling JSON into a Python dictionary (core logic). Instead, we can compose two separate functions:

>>> import json
>>> json.load(open('example.json'))

(Yes, if we were writing production code, we’d want to use open as a context manager to get automatic closing of file handles, but the approach above does a better job of illustrating the concept at hand, so Python pedants, put your pitchforks paway… er… away!)

Constructors without side effects are simpler than those with side effects

Typically, a constructor initializes the state when creating an instance from a class. For example:

>>> class Cat:
...     def __init__(self, sound='roowwwrrrr'):
...         self.sound = sound
...     def meow(self):
...         return f'Cat says: {self.sound}'
...
>>> num_num_cat = Cat(sound='num num')
>>> num_num_cat.meow()
'Cat says: num num'

Constructors with side effects—that is, which interact with external systems as part of initializing that internal state—force a developer to account for that external system when constructing the object. ā€œIs this external system available?ā€ ā€œWhat happens to the instance being constructed if it’s not available?ā€ For example:

>>> import requests
>>> class ApiClient:
...     def __init__(self):
...         self.token = requests.post('https://example.org/token', data={...})
...

In these cases, as described in Impure functions and/or classes are inescapable, so let’s minimize them, it’s simpler to move the interaction with the external system into a separate function, which would live in the imperative shell:

>>> def get_token(data):
...    return requests.post('https://example.org/token', data=data)

Now we can leverage composition to still initialize the token, but without complicating the constructor with side effects:

>>> class ApiClient:
...     def __init__(self, token):
...         self.token = token
...
>>> client = ApiClient(token=get_token(data={...}))

One final note on this: constructors with side effects are particularly pernicious because they are poison pills: they don’t just infect their own class, but any classes that depend on the constructor's class. For example:

>>> class TimeClient:
...     def __init__(self):
...         self.token = requests.post('https://example.org/token', data={...})
...
>>> class TimeService:
...     def __init__(self):
...         self.client = TimeClient()
...
>>> class TimeController:
...     def __init__(self):
...         self.service = TimeService()
...

Now it’s not just TimeClient() that makes an API call, it’s also TimeService() and TimeController(). (And yes, I’ll allow that dependency injection could also be used to alleviate issues in this particular example.) A practical example of where this ā€œtrickle upā€ effect will cause complexity: tests for both TimeService and TimeController now need to mock the API call. Which brings us to…

Tests with no mocking are simpler than tests with mocking

Mocking requires more code than not mocking. Code is complexity. Therefore, if we can test a unit without any mocks, our code is simpler than if we had to set up mocks. Mocks are required when our unit has dependencies. In other words, when it interacts with external systems. In other words, when it has side effects. In other words… when it is not a pure function.

One of the biggest simplicity advantages pure functions have over impure functions and classes is that there are no external dependencies to mock. Going back to our earlier example of a double() pure function versus a fetch_double() impure function, here’s what testing each would look like:

>>> def test_double():
...     assert double(2) == 4
...

Versus:

>>> from unittest import patch
>>> def test_fetch_double():
...     with patch('requests.post') as mock_post:
...         mock_post.return_value.status_code = 201
...         mock_post.return_value.json.return_value = {"answer": 4}
...         assert fetch_double(2) == 4
...

The first test is straightforward, simple, easy to read, and easy to understand. The second test involves knowing a great deal more than 2 Ɨ 2 = 4: you must know that successful HTTP requests should return a 2xx status code, that the requests library makes the returned value available via the json() method, the shape of the JSON object returned from the API, and the intricacies of where exactly to patch.

Mocking is often a code smell in tests. A large amount of mocking is stinky code; it is an indicator that a test subject’s dependencies have grown too complex. One mitigation strategy—that we've already seen in Constructors without side effects are simpler than those with side effects—would be to pull those dependencies into an imperative shell that could be tested by functional, integration, or end-to-end tests rather than unit tests.

Wrapping Up

Python has a bit of a love/hate relationship with functional programming. Going back to Guido van Rossum's 2013 Slashdot interview:

ā€œSo, mostly I don't think it makes much sense to try to add ā€˜functional’ primitives to Python, because the reason those primitives work well in functional languages don't apply to Python, and they make the code pretty unreadable for people who aren't used to functional languages (which means most programmers).ā€

That said, in the years since that 2013 interview, JavaScript and TypeScript have moved in a more functional-ish direction. As the most-used language(s) in the world, "most programmers" now have some familiarity with those functional programming primitives Guido rejected as ā€œunreadableā€.

Approached from a pragmatic perspective, those primitives—as demonstrated in the aforementioned tenets—can help steer a Python codebase towards more simplicity. After all:

ā€œSimple is better than complex.ā€

Resources

  • The Zen of Python
  • Guido van Rossum’s 2013 Slashdot interview
  • The fate of reduce() in Python 3000
  • Gary Bernhardt’s ā€œBoundariesā€ talk
  • Gary Bernhardt’s ā€œFunctional Core, Imperative Shellā€ screencast
  • Wikipedia’s entry on ā€œComposition over Inheritanceā€
  • unittest.mock’s docs on ā€œWhere to patchā€
  • Octoverse 2025
  • Awesome-functional-python
  • The Grokking Simplicity book

Kyle Adams is a staff software consultant at Test Double who lives for that light bulb moment when a solution falls perfectly in place or an idea takes root.

Related Insights

šŸ”—
Pydantically perfect: A beginner’s guide to Pydantic for Python type safety
šŸ”—
IndyPy Talk: Pydantically perfect in every way

Explore our insights

See all insights
Leadership
Leadership
Leadership
We're about to unwind fifty years of "Progress"

Every programming language, framework, and abstraction we've built exists to solve the same problem: humans have limited cognitive bandwidth. As agents become the primary authors of code, the rationale for all that scaffolding starts to weaken.

by
Doc Norton
Leadership
Leadership
Leadership
Field Report: All Things AI 2026

Test Double double agents provide a field report recap of the amazing 2026 All Things AI conference.

by
Anya Iverova
by
Cathy Colliver
by
Christine McCallum-Randalls
Leadership
Leadership
Leadership
What does "Good Code" even mean now?

For decades, we've optimized code for human readers. Agentic coding is forcing a renegotiation of what "craft" means, and that's shifting emphasis from code-level readability to system-level observability and problem-space incrementalism.

by
Doc Norton
Letter art spelling out NEAT

Join the conversation

Technology is a means to an end: answers to very human questions. That’s why we created a community for developers and product managers.

Explore the community
Test Double Executive Leadership Team

Learn about our team

Like what we have to say about building great software and great teams?

Get to know us
Test Double company logo
Improving the way the world builds software.
What we do
Services OverviewSoftware DeliveryProduct StrategyLegacy ModernizationPragmatic AIDevOpsUpgrade RailsTechnical RecruitmentAssessments
Who WE ARE
About UsCulture & CareersGreat CausesEDIOur TeamContact UsNews & AwardsN.E.A.T.
Resources
Case StudiesAll InsightsLeadership InsightsDeveloper InsightsProduct InsightsPairing & Office Hours
NEWSLETTER
Sign up hear about our latest innovations.
Your email has been added!
Oops! Something went wrong while submitting the form.
Standard Ruby badge
614.349.4279hello@testdouble.com
Privacy Policy
Ā© 2020 Test Double. All Rights Reserved.