Timeless Principles
Timeless Principles
“Fashions change, but principles endure.”
Learning Objectives
By the end of this module, you will be able to:
- Explain each principle and why it matters
- Recognize violations in code and design
- Apply these principles to your own work
- Balance principles when they conflict
Why “Timeless”?
Programming languages come and go. Frameworks rise and fall. The hot technology of today is the legacy system of tomorrow.
But some ideas persist across decades:
- The principle of simplicity predates computing
- DRY was articulated in 1999 and remains essential
- GIGO dates to the 1960s mainframe era
These principles work because they’re rooted in how humans think and how complex systems behave — not in any particular technology.
Learn them once. Apply them forever.
KISS: Keep It Simple, Stupid
The principle: Simplicity is a virtue. Complexity is a cost.
The name sounds insulting, but it’s self-directed: “Don’t be clever. Be clear.”
Why Simplicity Matters
- Reading: Simple code is easier to understand
- Debugging: Fewer interactions mean fewer hiding places for bugs
- Changing: Simple systems adapt more easily
- Teaching: Simple code can be explained to teammates
Simplicity in Practice
Complex:
def get_user_status(user):
return "active" if user and user.is_verified and not user.is_suspended and \
(user.last_login and (datetime.now() - user.last_login).days < 30) else \
"inactive" if user and not user.is_suspended else "suspended" if user else "unknown"
Simple:
def get_user_status(user):
if not user:
return "unknown"
if user.is_suspended:
return "suspended"
if not user.is_verified:
return "inactive"
if not user.last_login:
return "inactive"
if (datetime.now() - user.last_login).days >= 30:
return "inactive"
return "active"
Same logic. The second is readable. Each condition is clear. Debugging is straightforward.
The Trap of Cleverness
Clever code impresses no one who has to maintain it.
# "Clever"
result = [x for x in (y.value for y in items if y.active) if x > threshold]
# Clear
active_items = [item for item in items if item.active]
values = [item.value for item in active_items]
result = [value for value in values if value > threshold]
The clever version is one line. The clear version is three. The clear version is better.
Ask: “Will I understand this in six months?”
DRY: Don’t Repeat Yourself
The principle: Every piece of knowledge should have a single, authoritative source.
If you find yourself copying and pasting code, something is wrong.
Why Repetition Hurts
# Order processing
tax = price * 0.08
total = price + tax
# Invoice generation (different file)
tax = price * 0.08
total = price + tax
# Report calculation (yet another file)
tax = price * 0.08
total = price + tax
Three copies of the same calculation. Now:
- Tax rate changes to 9%
- You update order processing
- You forget to update invoices
- Reports show different numbers than invoices
- Hours of debugging
DRY in Practice
Extract the knowledge:
# config.py
TAX_RATE = 0.08
def calculate_total(price):
tax = price * TAX_RATE
return price + tax
Use it everywhere:
# order.py
from config import calculate_total
total = calculate_total(price)
# invoice.py
from config import calculate_total
total = calculate_total(price)
One source of truth. Change it once, updated everywhere.
DRY Beyond Code
DRY applies to:
- Configuration: Don’t hardcode the same value in multiple places
- Documentation: Don’t document the same thing twice (it will drift)
- Data: Don’t store the same data in multiple places
When DRY Can Be Wrong
Premature abstraction is also a problem. If two pieces of code look similar but represent different concepts, forcing them into one function creates coupling.
# These look similar but are different concepts
def calculate_employee_salary(hours, rate):
return hours * rate
def calculate_project_cost(hours, rate):
return hours * rate
Should these be one function? Maybe not. Employee salary and project cost might evolve differently. Sometimes repetition is acceptable if the concepts are distinct.
Rule of three: Wait until you have three instances before abstracting.
GIGO: Garbage In, Garbage Out
The principle: A system’s output quality is determined by its input quality.
If you feed bad data into a perfect algorithm, you get bad results.
Why This Matters
def calculate_average(numbers):
return sum(numbers) / len(numbers)
# Looks fine. But...
data = [100, 200, -999, 300, 400] # -999 is an error code, not a value!
average = calculate_average(data) # Returns 0.2 — completely wrong
The function worked correctly. The input was garbage.
Validate at the Boundaries
Don’t trust incoming data. Check it:
def calculate_average(numbers):
if not numbers:
raise ValueError("Cannot calculate average of empty list")
for n in numbers:
if not isinstance(n, (int, float)):
raise TypeError(f"Expected number, got {type(n)}")
if n < 0:
raise ValueError(f"Unexpected negative value: {n}")
return sum(numbers) / len(numbers)
Now garbage input produces a clear error instead of a silent wrong answer.
GIGO and AI
This principle is especially relevant for AI-assisted development:
- Garbage prompts → Garbage code: Vague instructions produce vague results
- Garbage code → Garbage output: AI-generated code working on bad data produces bad results
- Garbage review → Garbage merged: Not checking AI output lets bugs through
Quality in, quality out. At every stage.
YAGNI: You Aren’t Gonna Need It
The principle: Don’t build features until you need them.
The Temptation
You’re building a to-do app:
“What if users want to share lists? I should build sharing.” “What if they want to export to PDF? I should build that.” “What if they want to integrate with calendars? Better add that.”
You spend weeks on features. Then you discover:
- 80% of users just want a simple list
- No one asked for PDF export
- You never shipped because you were building hypotheticals
YAGNI in Practice
Don’t:
- Build features because you might need them
- Add configuration options “just in case”
- Create abstractions for hypothetical future requirements
Do:
- Build what’s needed now
- Make it easy to change later (simple code is changeable code)
- Add features when there’s actual demand
YAGNI and AI
AI assistants love to add features you didn’t ask for:
You: "Write a function to validate email addresses"
AI: "Here's an email validator with:
- Format validation
- DNS MX record checking
- Disposable email detection
- Spam trap identification
- ..."
You asked for format validation. AI gave you an enterprise email security suite.
Review AI output through YAGNI lens: “Did I ask for this? Do I need it now?”
Fail Fast
The principle: When something goes wrong, fail immediately and loudly.
The Alternative: Fail Slow
def process_payment(amount, user):
if amount is None:
amount = 0 # "Handle" the problem by using default
# Much later...
charge_card(user.card, amount) # Charges $0, user gets product free
The error was hidden. The system “worked” but did the wrong thing.
Fail Fast in Practice
def process_payment(amount, user):
if amount is None:
raise ValueError("Amount cannot be None")
if amount <= 0:
raise ValueError(f"Amount must be positive, got {amount}")
if not user.card:
raise ValueError("User has no card on file")
charge_card(user.card, amount)
Bad input? Immediate, clear failure. No silent corruption.
Why Fast Failure is Good
- Closer to the cause: The error happens where the problem is, not three functions later
- Clear message: You know exactly what went wrong
- No corruption: Bad data doesn’t spread through the system
- Easier debugging: Stack trace points to the actual issue
Separation of Concerns
The principle: Different responsibilities should be in different places.
The Monolith Problem
def handle_order(request):
# Parse the request
data = json.loads(request.body)
# Validate the data
if not data.get('items'):
return {"error": "No items"}
# Calculate prices
total = sum(item['price'] * item['quantity'] for item in data['items'])
tax = total * 0.08
final = total + tax
# Save to database
order = Order(items=data['items'], total=final)
db.save(order)
# Send confirmation email
email_body = f"Thank you for your order of ${final}"
send_email(data['email'], email_body)
# Log for analytics
analytics.log('order_placed', {'value': final})
return {"order_id": order.id}
This function does everything: parsing, validation, calculation, storage, email, analytics.
Problems:
- Hard to test (how do you test just the calculation?)
- Hard to change (what if email logic changes?)
- Hard to understand (too much in one place)
Separated Concerns
def handle_order(request):
data = parse_request(request)
validate_order_data(data)
order = create_order(data)
save_order(order)
send_confirmation(order)
track_order_analytics(order)
return {"order_id": order.id}
Each function has one job. Each can be tested independently. Each can change without affecting others.
Separation Heuristics
- If you’re describing a function with “and”, it might do too much
- “Validates and processes and saves and emails” → Split it
- If a change requires modifying multiple unrelated things, they’re coupled
- If you can’t test something in isolation, it has too many responsibilities
When Principles Conflict
Principles are guidelines, not laws. Sometimes they conflict:
DRY vs. KISS: Abstracting to avoid repetition can make code more complex.
- Judgment: Is the abstraction worth the complexity?
YAGNI vs. Good Design: Building only what’s needed might create technical debt.
- Judgment: Is this a foundation we’ll build on, or a throwaway experiment?
Fail Fast vs. User Experience: Crashing on every error is unfriendly.
- Judgment: Fail fast internally; present graceful errors to users.
The meta-principle: Use judgment. Principles inform decisions; they don’t make them for you.
Quick Reference
| Principle | Summary | Anti-Pattern |
|---|---|---|
| KISS | Simplicity over cleverness | Complex one-liners |
| DRY | Single source of truth | Copy-pasted code |
| GIGO | Validate input quality | Trust all data blindly |
| YAGNI | Build only what’s needed | Speculative features |
| Fail Fast | Immediate, clear errors | Silent failures |
| Separation of Concerns | One job per component | God functions |
Exercise: Spot the Violations
Exercise 1: Which Principle?
For each code smell, identify which principle is violated:
- A function that’s 300 lines long
- The tax rate
0.08appears in 12 different files - A form accepts any input without checking
- A feature nobody requested took two weeks to build
- An error is silently logged and execution continues
Answers
- KISS / Separation of Concerns (too complex, too many responsibilities)
- DRY (magic number repeated)
- GIGO (no input validation)
- YAGNI (built something not needed)
- Fail Fast (should have raised an error)
Exercise 2: Apply to Your Code
Review code you’ve written (or AI has generated for you).
Find one example of each:
- Something that could be simpler (KISS)
- Something that’s repeated (DRY)
- A place that should validate input (GIGO)
Key Insights
| Concept | Implication |
|---|---|
| KISS | Complexity is a cost; pay it only when necessary |
| DRY | One source of truth prevents inconsistency |
| GIGO | Validate inputs; don’t trust data blindly |
| YAGNI | Build for actual needs, not hypothetical ones |
| Fail Fast | Clear errors beat silent corruption |
| Separation of Concerns | One job per component enables change |
Reflection Questions
-
Why might an experienced developer write more code to keep things simple?
-
You see the same 5 lines of code in 3 places. Should you always abstract it into a function? What would make you say no?
-
How do these principles apply to working with AI assistants?
Next module: Communication — writing for humans, including your future self.