← Back to list

DRY: The Principle You’re Probably Applying Wrong

Avoiding knowledge duplication is not the same as avoiding code duplication, and confusing them is expensive!

Ricardo García Ramírez in Dev Genius · 2026-05-13 18:46 · 1 claps · 5.1 min read paywalled
#dry #best-practices #coding #python #coupling-tax
Open on Medium ↗
Wiki topics: 💻 · Programming

DRY: The Principle You’re Probably Applying Wrong

Avoiding knowledge duplication is not the same as avoiding code duplication, and confusing them is expensive!

The two-pipeline bug

You have two preprocessing pipelines. Both fill missing values with zero:

import pandas as pd

# pipeline_a.py
df_a = pd.DataFrame({"revenue": [100, None, 300]})
df_a["revenue"] = df_a["revenue"].fillna(0)
print(df_a["revenue"].tolist())

# pipeline_b.py
df_b = pd.DataFrame({"churn_flag": [1, None, 0]})
df_b["churn_flag"] = df_b["churn_flag"].fillna(0)
print(df_b["churn_flag"].tolist())
[100.0, 0.0, 300.0]
[1.0, 0.0, 0.0]

The code looks identical. Someone on your team extracts a utility function:

import pandas as pd

def fill_missing(series: pd.Series) -> pd.Series:
    return series.fillna(0)

df_a = pd.DataFrame({"revenue": [100, None, 300]})
df_a["revenue"] = fill_missing(df_a["revenue"])
print(df_a["revenue"].tolist())
[100.0, 0.0, 300.0]

Six months later, the data team discovers that missing churn flags should be treated as unknown, not zero. They need fillna(-1). They update fill_missing. Pipeline A breaks.

You’ve just paid the coupling tax! And you paid it for an abstraction that was never justified.

By the end of this article, you’ll have a concrete test you can apply before extracting any abstraction, one that tells you whether you’re eliminating a real DRY violation or creating a fake one.

What Hunt and Thomas actually said

DRY comes from The Pragmatic Programmer (Hunt & Thomas, 1999). The full statement:

“Every piece of knowledge must have a single, unambiguous, authoritative representation within a system.”

The keyword is knowledge: not code, not syntax, not lines. "Knowledge" means a business rule, a decision, or a fact about your domain. If you have to change a rule, you should only have to change it in one place.

It says nothing about how the code looks.

Knowledge duplication vs. code duplication

The two pipelines share syntax. They do not share knowledge.

  • Pipeline A encodes: missing revenue means zero revenue for accounting purposes.
  • Pipeline B encodes: missing churn flag means no churn recorded.

Different facts, different domains, same Python call. That’s incidental similarity: not duplication.

Contrast that with this:

# Two places encoding the same rule: minimum age is 18

def validate_user_age(age: int) -> str:
    if age < 18:
        return "REJECTED: must be 18 or older"
    return "OK"

def check_signup_eligibility(age: int) -> str:
    if age < 18:
        return "You must be at least 18 to sign up"
    return "Eligible"

print(validate_user_age(16))
print(check_signup_eligibility(16))
REJECTED: must be 18 or older
You must be at least 18 to sign up

Both encode the same business rule: minimum age is 18. If the legal minimum changes to 16, you update two places. That’s a real DRY violation. The code looks different; the knowledge is duplicated.

The coupling tax

Extracting code that only looks similar couples things that have no business being coupled. Every change to one now risks breaking the other. A utility module that starts with three functions ends up with fifteen, each one a coupling point between unrelated modules. The codebase becomes hard to change not because it’s complex, but because it’s entangled.

The three places this hurts most in data science code:

  1. Preprocessing utilities. fillna(0) for a revenue column and fillna(0) for a flag column are not the same decision.

  2. Model evaluation helpers. Two models sharing a metric function sounds fine until one needs weighted evaluation. A shared function makes that divergence painful.

  3. Config loading. Coupling two services to the same config location ties their lifecycles together. When one needs a different structure, you’re rewriting both.

The test

Before you extract an abstraction, apply this:

If the underlying rule changes, would I need to update this code in more than one place?

If yes, that’s a DRY violation. Extract it.

If no, the similarity is incidental. Leave it alone.

For the two pipelines, changing the fill value for revenue has no bearing on the churn decision. They’re independent. Do not extract.

For the age validation: changing the minimum age means updating two functions. Extract it:

MIN_LEGAL_AGE = 18  # Single authoritative source

def validate_user_age(age: int) -> str:
    if age < MIN_LEGAL_AGE:
        return f"REJECTED: must be {MIN_LEGAL_AGE} or older"
    return "OK"

def check_signup_eligibility(age: int) -> str:
    if age < MIN_LEGAL_AGE:
        return f"You must be at least {MIN_LEGAL_AGE} to sign up"
    return "Eligible"

print(validate_user_age(16))
print(check_signup_eligibility(16))
REJECTED: must be 18 or older
You must be at least 18 to sign up

Now if the rule changes, you change MIN_LEGAL_AGE. One place. Done.

Seeing the test in action

The shared utility breaks when the pipelines diverge:

import pandas as pd

def fill_missing(series: pd.Series) -> pd.Series:
    return series.fillna(-1)  # Changed for churn_flag — breaks pipeline_a

df_a = pd.DataFrame({"revenue": [100, None, 300]})
df_a["revenue"] = fill_missing(df_a["revenue"])
print("Pipeline A revenue:", df_a["revenue"].tolist())
assert df_a["revenue"].tolist() == [100.0, 0.0, 300.0], "Pipeline A is broken!"
Pipeline A revenue: [100.0, -1.0, 300.0]
AssertionError: Pipeline A is broken!  # traceback omitted for brevity

Independent pipelines each own their fill decision:

import pandas as pd

df_a = pd.DataFrame({"revenue": [100, None, 300]})
df_a["revenue"] = df_a["revenue"].fillna(0)

df_b = pd.DataFrame({"churn_flag": [1, None, 0]})
df_b["churn_flag"] = df_b["churn_flag"].fillna(-1)

assert df_a["revenue"].tolist() == [100.0, 0.0, 300.0]
assert df_b["churn_flag"].tolist() == [1.0, -1.0, 0.0]
print("Pipeline A revenue:", df_a["revenue"].tolist())
print("Pipeline B churn:  ", df_b["churn_flag"].tolist())
Pipeline A revenue: [100.0, 0.0, 300.0]
Pipeline B churn:   [1.0, -1.0, 0.0]

And changing MIN_LEGAL_AGE from 18 to 16 propagates with a single edit — both validate_user_age and check_signup_eligibility update automatically because they both reference the same constant.

Gotchas

Identical code is not a smell by itself. Two functions that sort a list the same way are not a DRY violation unless they’re expressing the same sorting rule.

Tests are a known exception. Sharing fixtures aggressively makes tests brittle and hard to read in isolation. Test code should usually tell its own story.

When NOT to DRY: early exploration. During prototyping, duplication is cheap. Copy-paste first. Extract when the pattern is stable and the second real use case exists.

The rule of three. Write it once. Note the similarity on the second occurrence. Extract on the third. One use case is never enough to know the right abstraction.

Takeaways

  • DRY is about knowledge duplication, not code duplication.
  • Identical code can be fine to repeat if it represents different decisions.
  • Different code can violate DRY if it encodes the same rule in two places.
  • Extract the knowledge, not the syntax.

What this leaves you with

The next time someone says “that code is duplicated, we should extract it,” you have a precise question: is it the same knowledge, or just the same syntax?

If it’s the same knowledge, extract without guilt. If it’s incidental similarity, leave it alone without guilt.

The YAGNI principle—covered next in this series—is the natural companion: it tells you when not to build the generalization even when the abstraction would be technically correct.

If you found this post helpful, don’t forget to 👏 clap to show your support!

I’d also love to hear your thoughts and insights on the techniques covered in this article. 💡 Feel free to share your experiences in the comments 💬.

Connect with Me on LinkedIn!

You can also connect with me on LinkedIn for updates on my latest posts and projects. 🌐 Let’s keep the conversation going!


메타데이터
post_id
a33ff11b41c8
slug
dry-the-principle-youre-probably-applying-wrong-a33ff11b41c8
url
https://blog.devgenius.io/dry-the-principle-youre-probably-applying-wrong-a33ff11b41c8
canonical_url
https://blog.devgenius.io/dry-the-principle-youre-probably-applying-wrong-a33ff11b41c8
author_url
https://medium.com/@rgr5882
status
ok
fetched_at
2026-06-10 21:21:38