Skip to content
wannacode

Names in code

In short: Good names in code say what a thing is and why it exists: constants instead of magic numbers, booleans phrased as questions and the words of the business instead of abbreviations.

Problem

The function nobody dares to touch

In the code of an online shop there is a function that every order goes through:

def calc(a, t, f):
d = 4.99
if a >= 50:
d = 0
if t == 2:
d += 7
if f:
d = 0
return a + d
print(f"€{calc(40, 1, False):.2f}")
print(f"€{calc(40, 2, False):.2f}")
print(f"€{calc(60, 2, False):.2f}")
print(f"€{calc(40, 2, True):.2f}")
Output
€44.99
€51.99
€67.00
€40.00

It works. Now say what it does. What is t? Why does t == 2 add 7? What is f, and why does it reset everything? Why exactly 50?

The author knew. He left a year ago. Marketing wants free delivery from €60, not from €50. Your colleague searches the project for 50 and finds 14 places: in one, 50 is the delivery threshold; in another, it is the largest parcel weight in kilograms; in a third, it is the number of products on a page. Which one should change?

The logic here is right. The problem is that the code does not tell the person reading it what the author was thinking.

The idea

The name says what and why, not how

The computer reads calc(a, t, f) with no trouble. A person can't. And code is read many more times than it is written: at every fix, at every review, every time someone looks for a bug.

A good name answers the questions you would otherwise ask the author: what is this, why is it here, how is it used. This is called an intention-revealing name. Here is the same function, with only the names changed:

def total_with_delivery(subtotal, delivery_type, has_free_delivery_coupon):
delivery_fee = 4.99
if subtotal >= 50:
delivery_fee = 0
if delivery_type == 2:
delivery_fee += 7
if has_free_delivery_coupon:
delivery_fee = 0
return subtotal + delivery_fee
print(f"€{total_with_delivery(40, 1, False):.2f}")
print(f"€{total_with_delivery(40, 2, False):.2f}")
print(f"€{total_with_delivery(60, 2, False):.2f}")
print(f"€{total_with_delivery(40, 2, True):.2f}")
Output
€44.99
€51.99
€67.00
€40.00

The output is the same down to the last digit. The function now reads like a rule: delivery costs €4.99, it is free from €50, express delivery costs €7 more, and the coupon makes it free.

Names that don't help:

  • a name that says how, not what: float_value, list1, tmp;
  • a general name that fits anything: data, info, value, process, handle, manager;
  • an abbreviation only the author knows: calc, cnt, dlv_fee.

The length follows the scope. i in a three-line loop is fine. A parameter of a function called from ten places deserves a full name.

More precisely, if you are curious

What about a comment above the function, t: 1 = standard, 2 = express? It helps until someone changes the numbers and forgets the comment. A name can't go out of date separately from the code, because it is part of the code. When a comment helps and when it lies is the topic of the third lesson in this module.

The idea

Magic number → named constant

The names of the variables are in place now, but 4.99, 50, 7 and 2 are still in the code. A number without a name in the code is called a magic number: only the author knows what it means, and when it has to change, you search for it everywhere.

The fix is a named constant. You write the value once, in one place, and use the name everywhere else:

FREE_DELIVERY_FROM = 50
STANDARD_DELIVERY_FEE = 4.99
EXPRESS_SURCHARGE = 7
STANDARD = 1
EXPRESS = 2
Python has no constants. The capital letters, UPPER_SNAKE_CASE, are a convention from PEP 8: "this value does not change". The language won't stop you, but everyone who reads the code knows what they mean.

Marketing wants €60: you change one line, and every place that asks "where does free delivery start" sees the new value. And the 50s that are weights stay untouched, because they are not FREE_DELIVERY_FROM.

More precisely, if you are curious

Not every number is magic. 0 for "no fee", 1 in i + 1 and 100 in a percentage are clear from where they are. A number is magic when it carries a business rule (a threshold, a fee, a limit, a status code) or may change. A constant named after its value (FIFTY = 50) doesn't help: the name says the role, not the number.

Nothing stops a second assignment:

FREE_DELIVERY_FROM = 50
FREE_DELIVERY_FROM = 60
print(FREE_DELIVERY_FROM)
Output
60

typing.Final (FREE_DELIVERY_FROM: Final = 50) makes a tool like mypy complain about the second assignment. The program itself still accepts it.

See it

The same calculation, read through the names

The names and the constants together:

FREE_DELIVERY_FROM = 50
STANDARD_DELIVERY_FEE = 4.99
EXPRESS_SURCHARGE = 7
STANDARD = 1
EXPRESS = 2
def total_with_delivery(subtotal, delivery_type, has_free_delivery_coupon):
delivery_fee = STANDARD_DELIVERY_FEE
if subtotal >= FREE_DELIVERY_FROM:
delivery_fee = 0
if delivery_type == EXPRESS:
delivery_fee += EXPRESS_SURCHARGE
if has_free_delivery_coupon:
delivery_fee = 0
return subtotal + delivery_fee
print(f"€{total_with_delivery(40, STANDARD, False):.2f}")
print(f"€{total_with_delivery(40, EXPRESS, False):.2f}")
print(f"€{total_with_delivery(60, EXPRESS, False):.2f}")
print(f"€{total_with_delivery(40, EXPRESS, True):.2f}")
Output
€44.99
€51.99
€67.00
€40.00

Watch every number and every letter from the first version get a meaning as the calculation goes:

FREE_DELIVERY_FROM = 50
STANDARD_DELIVERY_FEE = 4.99
EXPRESS_SURCHARGE = 7
STANDARD = 1
EXPRESS = 2
def total_with_delivery(subtotal, delivery_type, has_free_delivery_coupon):
delivery_fee = STANDARD_DELIVERY_FEE
if subtotal >= FREE_DELIVERY_FROM:
delivery_fee = 0
if delivery_type == EXPRESS:
delivery_fee += EXPRESS_SURCHARGE
if has_free_delivery_coupon:
delivery_fee = 0
return subtotal + delivery_fee
print(f"€{total_with_delivery(40, EXPRESS, False):.2f}")
Step by step, in words
  1. The old call was calc(40, 2, False). Now it says what it calculates, and 2 is EXPRESS.
  2. The parameters: a, t, f become subtotal, delivery_type, has_free_delivery_coupon. The values are the same.
  3. "d = 4.99" becomes "delivery_fee = STANDARD_DELIVERY_FEE". The same 4.99, but now you know what it is: the standard delivery fee.
  4. 50 is FREE_DELIVERY_FROM: the threshold for free delivery. 40 is below it, so the delivery is not free.
  5. "t == 2" was a riddle. delivery_type == EXPRESS is a sentence: express delivery adds EXPRESS_SURCHARGE.
  6. "if f:" told you nothing. has_free_delivery_coupon is a question, and the answer is False: no coupon.
  7. The order plus the delivery. The program prints €51.99.
  8. Back and forth: the same values, calculation and output as calc(40, 2, False). Only the names changed.

All three versions print the same thing. The computer sees no difference. The difference is for the person who changes the rule tomorrow. One place still says nothing: False in the call. We get to it in the next lesson.

Try it

Predict the output

An order of exactly €50 with standard delivery, and an order of €49.90 with express delivery. Read the rule through the names. What does the program print?

print(f"€{total_with_delivery(50, STANDARD, False):.2f}")
print(f"€{total_with_delivery(49.90, EXPRESS, False):.2f}")
Answer

Try it

Fill in the blank

The threshold for free delivery is a constant now. What goes in the blank?

def total_with_delivery(subtotal, delivery_type, has_free_delivery_coupon):
    delivery_fee = STANDARD_DELIVERY_FEE
    if subtotal >= :
        delivery_fee = 0
    if delivery_type == EXPRESS:
        delivery_fee += EXPRESS_SURCHARGE
    if has_free_delivery_coupon:
        delivery_fee = 0
    return subtotal + delivery_fee

The idea

A boolean is a question

A boolean answers yes or no, so its name is a question: is_paid, has_coupon, can_ship. Then the if reads like a sentence:

subtotal = 62.40
is_paid = True
qualifies_for_free_delivery = subtotal >= FREE_DELIVERY_FROM
if is_paid and qualifies_for_free_delivery:
print("We ship with no delivery fee.")
Output
We ship with no delivery fee.

What doesn't work:

  • flag, status, check, ok: they don't say what the question is;
  • a negative in the name: is_not_paid. Then if not is_not_paid: is a double negative, and everyone trips over it. Write is_paid, and put the negation in the check when you need it.

The same goes for the other names. A class and a variable are nouns (Order, delivery_fee). A function does something, so it is a verb (send_invoice). A function named like a question (is_in_stock) promises that it only asks. In the next lesson you will see what happens when it breaks that promise.

The idea

The words of the shop in the code

You don't invent the best names. They are the words of the people the program is for: parcel, courier, cash on delivery, refund, invoice. When the code says parcel, courier, refund, the programmer and the person in the warehouse talk about the same thing. These are domain names.

  • One word for one concept. If the customer is customer, it doesn't become client in the next file and buyer in the database. If reading is find…, it doesn't become get… and fetch… elsewhere. A different word makes the reader look for a different thing.
  • One word doesn't mean two things. If total is the sum with delivery, the sum without it has its own name, subtotal.
  • Technical words are for technical things. queue, cache, parser are fine when that is the thing itself. Manager, Processor and Helper usually mean "I don't know what exactly it does".

Try it

Find the bug

Accounting complains: an invoice for €50 without VAT comes out at €110. VAT is 20%, so it should be €60.

def vat(price):
return price * 1.20
def print_invoice(net):
total = net + vat(net)
print(f"Net: €{net:.2f}")
print(f"Total with VAT: €{total:.2f}")
print_invoice(50)
Output
Net: €50.00
Total with VAT: €110.00

Which line is the bug on?

Pick the line with the bug.

The idea

How your language writes names

A good name in one language looks odd in another. Each language has its own convention, and code that follows it reads faster, because the shape of the name tells you what it is.

The convention comes from PEP 8:

  • functions, methods, variables and modules: snake_case (total_with_delivery, delivery_fee);
  • classes: PascalCase (DeliveryCalculator);
  • module-level constants: UPPER_SNAKE_CASE (FREE_DELIVERY_FROM);
  • a leading _ (_cache) means "internal, don't use it from outside". The language doesn't forbid it, it is a convention.
More precisely, if you are curious

The same name in the four languages:

Java C# Python JavaScript
function totalWithDelivery TotalWithDelivery total_with_delivery totalWithDelivery
variable deliveryFee deliveryFee delivery_fee deliveryFee
constant FREE_DELIVERY_FROM FreeDeliveryFrom FREE_DELIVERY_FROM FREE_DELIVERY_FROM
class Order Order Order Order
boolean isPaid isPaid (property: IsPaid) is_paid isPaid

The idea

Rename: the cheapest refactoring

Changing a name without changing the behavior is called Rename. It is the smallest and cheapest refactoring: you don't change a single value or comparison, and the program prints the same thing.

Don't do it by hand with find and replace. The editor knows which d is your variable and which one is the letter in "delivery": in IntelliJ IDEA and PyCharm it is Shift+F6, in VS Code it is F2, in Visual Studio it is Ctrl+R, Ctrl+R. It changes every place at once.

There is no compiler here to catch a missed place. The old name gives an error (NameError) only when that line runs. So after a Rename you run the program, or better, the tests. And a name that comes as text (getattr(order, "fee"), a key in JSON) is invisible to the editor.

How to refactor in small steps, so that the program works after each one, is the topic of the sixth lesson in this module. Rename is the first step.

Exercise

The courier

Solve the task in your editor.

The courier. This function calculates the price of a parcel:

def f(w, z):
p = 3.50
if w > 2:
p += (w - 2) * 0.80
if z == 3:
p *= 1.5
return p
print(f"€{f(1.5, 1):.2f}")
print(f"€{f(5, 1):.2f}")
print(f"€{f(5, 3):.2f}")
Output
€3.50
€5.90
€8.85

The rule is this: a parcel up to 2 kg costs €3.50, every kilogram above that is €0.80, and zone 3 is remote, so the price there is 50% higher. Give the function, the parameters and the variable names that say this, and turn the numbers into constants. The output must stay the same:

Output
€3.50
€5.90
€8.85

More tasks

  • The boolean. Add a parameter that says whether the parcel is fragile (a €1.20 fee). What will you call it? What will the call look like?
  • Your code. Open an old exercise of yours. Find three names you don't understand at first glance today, and change them with the editor's Rename. Run the program: is the output the same?
  • The shop's dictionary. Write down ten words the person in the warehouse uses (parcel, courier, zone…) and how you would name them in the code of your language.

Test

Check yourself

1. A variable holds the sum of the products after the discount, but without delivery. Which name is best?
2. What is wrong with if not is_not_paid:?
3. Free delivery now starts at €60 instead of €50. How does the constant help?
4. get_user(email) finds the user, and if there is none, it creates a new one and saves it to the database. What is the problem?
5. Why is Rename the cheapest refactoring?

Made by Ivo Gergov as part of Evo Tech's campaign for accessible programming education ·