Clean code is code that other developers can read and understand quickly. It is not about cleverness. It is about clarity. The best code reads like a well-written paragraph.
These 10 rules will make your code better immediately. Each rule includes bad and good examples in both Kotlin and Python. You can apply these today, no matter what language you use.
Rule 1: Use Descriptive Names
Names should tell you what a variable, function, or class does. If you need a comment to explain a name, the name is wrong.
Bad:
// Kotlin — what does this mean?
val d = 7
val tp = 49.99
fun calc(a: Int, b: Double): Double = a * b
# Python — impossible to understand without context
d = 7
tp = 49.99
def calc(a, b):
return a * b
Good:
// Kotlin — instantly clear
val daysUntilDeadline = 7
val ticketPrice = 49.99
fun calculateTotalCost(quantity: Int, pricePerItem: Double): Double {
return quantity * pricePerItem
}
# Python — anyone can read this
days_until_deadline = 7
ticket_price = 49.99
def calculate_total_cost(quantity: int, price_per_item: float) -> float:
return quantity * price_per_item
Key tips:
- Use full words, not abbreviations (
customernotcust) - Variable names should describe the data they hold
- Function names should describe what they do (use verbs)
- Class names should describe what they represent (use nouns)
- Boolean names should read as yes/no questions (
is_active,hasPermission)
Rule 2: Keep Functions Small and Focused
A function should do one thing. If you can describe what a function does using the word “and”, it does too much.
Bad:
// Kotlin — this function does THREE things
fun processOrder(order: Order) {
// Validate
if (order.items.isEmpty()) throw IllegalArgumentException("No items")
if (order.total < 0) throw IllegalArgumentException("Invalid total")
// Calculate
var total = 0.0
for (item in order.items) {
total += item.price * item.quantity
if (item.quantity > 10) total *= 0.9
}
// Save
database.save(order.copy(total = total, status = "CONFIRMED"))
emailService.send(order.customerEmail, "Order confirmed!")
}
Good:
// Kotlin — each function has ONE job
fun processOrder(order: Order) {
validateOrder(order)
val total = calculateTotal(order.items)
confirmOrder(order, total)
}
private fun validateOrder(order: Order) {
require(order.items.isNotEmpty()) { "Order must have items" }
require(order.total >= 0) { "Total cannot be negative" }
}
private fun calculateTotal(items: List<OrderItem>): Double {
return items.sumOf { item ->
val subtotal = item.price * item.quantity
if (item.quantity > 10) subtotal * 0.9 else subtotal
}
}
private fun confirmOrder(order: Order, total: Double) {
database.save(order.copy(total = total, status = "CONFIRMED"))
emailService.send(order.customerEmail, "Order confirmed!")
}
Guidelines:
- Functions should be 5-20 lines (with rare exceptions)
- If a function has more than 3 parameters, consider using a data class or object
- Functions at the same level of abstraction should be together
Rule 3: Do Not Repeat Yourself (DRY)
Duplicated code means duplicated bugs. When you fix something in one place, you must remember to fix it everywhere else. You will forget.
Bad:
# Python — the same validation logic in three places
def create_user(name, email):
if not name or len(name) < 2:
raise ValueError("Name must be at least 2 characters")
if not email or "@" not in email:
raise ValueError("Invalid email address")
return database.insert_user(name, email)
def update_user(user_id, name, email):
if not name or len(name) < 2:
raise ValueError("Name must be at least 2 characters")
if not email or "@" not in email:
raise ValueError("Invalid email address")
return database.update_user(user_id, name, email)
Good:
# Python — validation logic in one place
def validate_user_input(name: str, email: str) -> None:
if not name or len(name) < 2:
raise ValueError("Name must be at least 2 characters")
if not email or "@" not in email:
raise ValueError("Invalid email address")
def create_user(name: str, email: str):
validate_user_input(name, email)
return database.insert_user(name, email)
def update_user(user_id: int, name: str, email: str):
validate_user_input(name, email)
return database.update_user(user_id, name, email)
When NOT to apply DRY: If two pieces of code look similar but serve different purposes, they might change independently. Do not force them together just because they look the same. Wait until you see the pattern three times.
Rule 4: Handle Errors Properly
Never ignore errors. Never use empty catch blocks. Every error should either be handled or passed up to the caller.
Bad:
// Kotlin — swallowing exceptions is dangerous
fun loadConfig(): Config {
try {
val json = File("config.json").readText()
return Json.decodeFromString(json)
} catch (e: Exception) {
// This hides the real problem
return Config() // default config, but WHY did it fail?
}
}
Good:
// Kotlin — handle errors with useful information
fun loadConfig(path: String): Result<Config> {
return runCatching {
val json = File(path).readText()
Json.decodeFromString<Config>(json)
}.onFailure { error ->
logger.error("Failed to load config from $path: ${error.message}")
}
}
// Caller decides what to do
val config = loadConfig("config.json").getOrElse { error ->
logger.warn("Using default config: ${error.message}")
Config.default()
}
Bad:
# Python — bare except catches EVERYTHING, including KeyboardInterrupt
try:
result = do_something()
except:
pass
Good:
# Python — catch specific exceptions, provide context
try:
result = do_something()
except ValueError as e:
logger.error(f"Invalid value in do_something: {e}")
raise
except ConnectionError as e:
logger.warning(f"Connection failed, retrying: {e}")
result = retry_do_something()
Error handling rules:
- Catch specific exceptions, not generic ones
- Log errors with useful context (what happened, what was the input)
- Fail fast — detect problems early and report them clearly
- Never use empty catch blocks
- Use the language’s error types (Result in Kotlin, exceptions in Python)
Rule 5: Write Comments Only When Necessary
Good code is self-documenting. Comments should explain “why”, not “what”. If you need to explain what the code does, the code is not clear enough.
Bad:
// Kotlin — these comments add nothing
// Get the user
val user = repository.getUser(id)
// Check if user is null
if (user == null) {
// Return not found
return HttpStatusCode.NotFound
}
// Return the user
return user
Good:
// Kotlin — the code explains itself, comment explains WHY
val user = repository.getUser(id) ?: return HttpStatusCode.NotFound
// Cache for 5 minutes because user profiles rarely change
// but the profile page is our most visited endpoint
cache.set("user:$id", user, ttl = 5.minutes)
When comments ARE useful:
- Explaining business rules that are not obvious from the code
- Warning about non-obvious consequences (“this function is called from multiple threads”)
- Explaining why a seemingly odd approach was chosen
- TODO comments for known improvements
- Public API documentation
When to avoid comments:
- Restating what the code does in English
- Commenting out code (delete it, Git has history)
- Journal comments (“changed on March 15 by Alex”)
Rule 6: Use Early Returns
Deeply nested if/else blocks make code hard to read. Use early returns to handle edge cases first, then write the main logic at the top level.
Bad:
# Python — the "arrow" anti-pattern
def process_payment(order):
if order is not None:
if order.is_valid():
if order.has_items():
if order.customer.has_payment_method():
total = calculate_total(order)
if total > 0:
return charge_customer(order.customer, total)
else:
return Error("Total must be positive")
else:
return Error("No payment method")
else:
return Error("No items in order")
else:
return Error("Invalid order")
else:
return Error("Order is None")
Good:
# Python — flat and readable with early returns
def process_payment(order):
if order is None:
return Error("Order is None")
if not order.is_valid():
return Error("Invalid order")
if not order.has_items():
return Error("No items in order")
if not order.customer.has_payment_method():
return Error("No payment method")
total = calculate_total(order)
if total <= 0:
return Error("Total must be positive")
return charge_customer(order.customer, total)
The good version is flat. Each check is at the same level. The happy path is the last line. This pattern is sometimes called “guard clauses”.
Rule 7: Follow the Single Responsibility Principle
A class or module should have one reason to change. If a class handles user data AND sends emails AND writes logs, it has three reasons to change.
Bad:
// Kotlin — this class does too many things
class UserManager(
private val database: Database,
private val emailClient: EmailClient,
) {
fun createUser(name: String, email: String): User {
// Validation
require(name.isNotBlank()) { "Name required" }
require(email.contains("@")) { "Invalid email" }
// Database operation
val user = User(name = name, email = email)
database.insert("users", user)
// Send email
val html = "<h1>Welcome, $name!</h1><p>Thanks for joining.</p>"
emailClient.send(to = email, subject = "Welcome!", body = html)
return user
}
}
Good:
// Kotlin — each class has ONE responsibility
class UserValidator {
fun validate(name: String, email: String) {
require(name.isNotBlank()) { "Name required" }
require(email.contains("@")) { "Invalid email" }
}
}
class UserRepository(private val database: Database) {
fun save(user: User): User {
database.insert("users", user)
return user
}
}
class WelcomeEmailSender(private val emailClient: EmailClient) {
fun send(user: User) {
emailClient.send(
to = user.email,
subject = "Welcome!",
body = "Welcome, ${user.name}! Thanks for joining."
)
}
}
class UserService(
private val validator: UserValidator,
private val repository: UserRepository,
private val welcomeEmail: WelcomeEmailSender,
) {
fun createUser(name: String, email: String): User {
validator.validate(name, email)
val user = repository.save(User(name = name, email = email))
welcomeEmail.send(user)
return user
}
}
Benefits: Each class is easy to test, easy to change, and easy to reuse. If the email template changes, you only touch WelcomeEmailSender. If the database changes, you only touch UserRepository.
Rule 8: Prefer Composition Over Inheritance
Inheritance creates tight coupling. When a parent class changes, all children might break. Use composition (having objects as fields) instead of inheritance (extending classes).
Bad:
# Python — deep inheritance hierarchy
class Animal:
def eat(self):
print("Eating")
class Bird(Animal):
def fly(self):
print("Flying")
class Penguin(Bird):
def fly(self):
# Penguins cannot fly, but they inherit fly()
raise NotImplementedError("Penguins cannot fly")
def swim(self):
print("Swimming")
Good:
# Python — composition with clear capabilities
class EatingBehavior:
def eat(self):
print("Eating")
class FlyingBehavior:
def fly(self):
print("Flying")
class SwimmingBehavior:
def swim(self):
print("Swimming")
class Eagle:
def __init__(self):
self.eating = EatingBehavior()
self.flying = FlyingBehavior()
class Penguin:
def __init__(self):
self.eating = EatingBehavior()
self.swimming = SwimmingBehavior()
# No flying behavior — because penguins do not fly
When inheritance IS fine:
- When there is a true “is-a” relationship
- When the hierarchy is shallow (one level deep)
- When the base class is abstract or an interface
Rule 9: Write Tests for Your Code
Tests are not extra work. They are part of the code. Untested code is code you cannot refactor with confidence.
Good test example:
// Kotlin — clear, focused tests
class CalculatorTest {
private val calculator = Calculator()
@Test
fun `add returns sum of two numbers`() {
assertEquals(5, calculator.add(2, 3))
}
@Test
fun `add handles negative numbers`() {
assertEquals(-1, calculator.add(2, -3))
}
@Test
fun `divide throws exception for zero divisor`() {
assertThrows<ArithmeticException> {
calculator.divide(10, 0)
}
}
}
# Python — test with clear names and structure
class TestCalculator:
def setup_method(self):
self.calculator = Calculator()
def test_add_returns_sum(self):
assert self.calculator.add(2, 3) == 5
def test_add_handles_negative_numbers(self):
assert self.calculator.add(2, -3) == -1
def test_divide_raises_for_zero(self):
with pytest.raises(ZeroDivisionError):
self.calculator.divide(10, 0)
Testing rules:
- Each test should test ONE thing
- Test names should describe what is being tested and the expected result
- Follow the Arrange-Act-Assert pattern
- Test edge cases (empty inputs, null, zero, negative numbers)
- Tests should be independent — one test should not depend on another
Rule 10: Keep It Simple
The simplest solution that works is the best solution. Do not over-engineer. Do not add abstractions you do not need yet.
Bad:
// Kotlin — over-engineered for a simple task
interface StringFormatter {
fun format(input: String): String
}
class UpperCaseFormatter : StringFormatter {
override fun format(input: String) = input.uppercase()
}
class StringFormatterFactory {
fun create(type: String): StringFormatter {
return when (type) {
"upper" -> UpperCaseFormatter()
else -> throw IllegalArgumentException("Unknown type: $type")
}
}
}
// Usage
val formatter = StringFormatterFactory().create("upper")
val result = formatter.format("hello")
Good:
// Kotlin — just do it
val result = "hello".uppercase()
When to add complexity:
- When you have a real requirement, not a hypothetical one
- When the same pattern appears three or more times
- When tests show you need a seam for dependency injection
- When the team agrees the abstraction helps readability
YAGNI — You Ain’t Gonna Need It. Do not build for features you might need someday. Build for what you need today.
Quick Reference
| Rule | Summary |
|---|---|
| 1. Descriptive names | Names should explain themselves |
| 2. Small functions | One function, one job |
| 3. DRY | Do not repeat logic |
| 4. Error handling | Catch specific errors, provide context |
| 5. Minimal comments | Explain why, not what |
| 6. Early returns | Flatten nested conditions |
| 7. Single responsibility | One class, one reason to change |
| 8. Composition over inheritance | Prefer has-a over is-a |
| 9. Write tests | Test one thing per test |
| 10. Keep it simple | No premature abstraction |
Related Articles
- Kotlin Tutorial Series — learn Kotlin from basics to advanced
- Python Tutorial Series — learn Python step by step
- Kotlin Testing
- Python Testing
- MVI with Jetpack Compose — clean architecture in practice
What’s Next?
Pick the rule you struggle with most and focus on it for a week. Review your recent code and apply that one rule everywhere. Then move on to the next.
Clean code is a habit, not a skill you learn once. The more you practice, the more natural it becomes. Your future self — and your teammates — will thank you.