Embercode
✕
Functions and ScopeLesson 9 of 17

40 min read

Static methods and helper modules in Python

Why this matters

Most programs need small helper jobs done again and again: counting vowels in a word, keeping a number inside a range, checking whether a string reads the same backwards. You could copy the code each time, or you could write it once as a function and call it from anywhere.

Python has no static keyword for methods. It has two tools instead. One is a plain function at the top level of a file. The other is a method inside a class that is marked with the @staticmethod decorator. A decorator is a line starting with @ that changes the function defined below it. Knowing when to use each tool decides how your code is organised. A helper that should belong to an object, but does not, leads to designs where data is passed around for no reason. A helper that needs no object, but is written so that it needs one, forces you to create an object just to run a calculation. Both mistakes appear in beginner code and in code reviews.

If you know Java, @staticmethod is the closest match to a Java static method. Python does not need a utility class, as the module section shows.

This lesson covers helper functions that stand alone, modules that hold them, static methods inside a class, and the point where a helper should become a regular method on an object. The lesson on pure functions, earlier in this module, covers purity in more detail.

The idea: a function that does not need an object

An object is a value built from a class, such as a Cart built with Cart(). A method is a function defined inside a class. A regular method runs on one object, and it can read that object's attributes, which are the values stored on the object. The first parameter of a regular method is self, the name for the object the method is running on. You call it with a dot after the object: cart.total().

A plain function is not inside a class. It does not get self, and you call it by its name:

Python · editable

Output:

36

square takes a number and returns its square. It uses only its input, and it does not need any stored data, so it does not need an object. Python's built-in functions work the same way. max, len and str are called by name, as in max(3, 7). The math module holds more functions, such as math.sqrt(16). The name before the dot there is a module, not an object.

Think of a kitchen scale in a shop. Anyone can use it, and it always works the same way. You do not need to own a particular scale to weigh an apple. A plain function works like the scale. A regular method is more like a bank account. Its balance belongs to one account, and you must go to that account to see or change it.

A module as a group of helpers

A module is a single .py file. Every name defined at the top level of the file belongs to that module, and other files can import it. Grouping related helpers in one module gives the same benefit as a utility class in other languages: a reader knows where to look, and the name at the call site says what the helper does.

Here are three files. The first two hold helpers, and the third uses them.

text_utils.py:

def count_vowels(text):
    count = 0
    for ch in text.lower():
        if ch in "aeiou":
            count += 1
    return count
 
def is_palindrome(text):
    cleaned = text.lower()
    return cleaned == cleaned[::-1]
 
def repeat(text, times):
    return text * times

math_utils.py:

def clamp(value, low, high):
    return max(low, min(value, high))

main.py:

import text_utils
from math_utils import clamp
 
print(text_utils.count_vowels("Hello World"))
print(text_utils.is_palindrome("level"))
print(text_utils.is_palindrome("Java"))
print(text_utils.repeat("ab", 3))
print(clamp(15, 0, 10))
print(clamp(-4, 0, 10))
print(clamp(7, 0, 10))

Output when main.py runs from the folder that holds the three files:

3
True
False
ababab
10
0
7

The line import text_utils brings in the whole module, so each call is written with the module name in front, such as text_utils.count_vowels(...). The line from math_utils import clamp brings in one name, so clamp(...) needs no prefix. Both styles are common.

Some details of the code. count_vowels lowercases the text, so H and h are treated the same, and it counts the letters e, o and o in "Hello World", which gives 3. cleaned[::-1] is a slice. A slice takes part of a string or list, and the step of -1 makes it go backwards, so the comparison checks whether the word reads the same both ways. clamp uses min to cap the value at high, and max to lift it to at least low. Python prints booleans as True and False, with capital letters.

If main.py calls a helper that it did not import, Python does not know the name. Here is a file that uses count_vowels without importing it:

from text_utils import is_palindrome
 
print(count_vowels("abc"))

Output, with the directory part of the file path left out:

Traceback (most recent call last):
  File "caller.py", line 3, in <module>
    print(count_vowels("abc"))
          ^^^^^^^^^^^^
NameError: name 'count_vowels' is not defined

NameError means Python could not find the name anywhere it looks. The fix is to import the name, or to write text_utils.count_vowels with the module name in front.

A module is the Python way to group helpers, and it is shorter than a class. Python has no private constructor, and a module is never built into an object. A name that starts with an underscore, such as _normalise, tells readers that the name is meant for use inside the file. Python does not block other code from using it.

A second example: temperature conversions

The next example is a set of temperature helpers in one file. Each helper takes a number and returns a number or a word. None of them needs stored data, so none of them needs an object.

Python · editable

Output:

212.0
100.0
hot
16

The first two lines check the conversions. 100 degrees Celsius is 212 degrees Fahrenheit, and 212 degrees Fahrenheit is 100 degrees Celsius. The division operator / always gives a float in Python, which is a number with a decimal point, so the results print as 212.0 and 100.0. The third line converts 30 Celsius to 86 Fahrenheit, and describe returns hot for any value at or above 80.

The last line calls square(4) with no prefix, because square is defined in the same file. A function in another file needs an import first, as the module section showed.

Static methods in a class

Sometimes a helper belongs next to a class, because readers will look for it there, but it still needs no object. Python handles this with the @staticmethod decorator. A static method has no self parameter. It can be called through the class name, and it cannot read or change the attributes of an object, because it never receives one.

Inside a static method, Python only knows three kinds of names:

  • the method's own parameters,
  • the method's own local variables,
  • names defined at the top of the same file, such as other functions and module-level variables.

A static method cannot use self, cannot read attributes such as self.count, and cannot call a regular method directly, because it has no object to call the method on. Here is a class that tries all three in one place:

class Two:
    def __init__(self):
        self.count = 5
 
    @staticmethod
    def read_count():
        return self.count

Calling Two.read_count() fails with NameError: name 'self' is not defined. The name self only exists inside a regular method, and read_count is not one. This rule causes most of the errors in this lesson.

When a helper belongs on an object

This is the main decision in this lesson. If a helper reads or changes the attributes of one particular object, it should be a regular method. If it needs only its input values, it can be a plain function or a static method.

Here is a shopping cart. It stores items, and it has a total. The total depends on the items in that particular cart, so it is a regular method. A helper that adds a list of prices together needs only its input, so it is a static method.

Python · editable

Output:

Total: 12.5
Item count: 2
Sum of prices: 4.75

The total is 12.5 because four pens at 1.50 cost 6.00 and two notebooks at 3.25 cost 6.50. The total method reads the three lists stored on this particular cart, so each cart has its own total. zip pairs up the prices and the quantities one item at a time, and sum adds the products together. sum_prices does not look at any cart. It receives a list and returns the sum, so it is a static method, and it is called as Cart.sum_prices(...) without creating a cart.

The f-strings such as f"Total: {cart.total()}" put the result of an expression inside the text. The part in braces is evaluated when the line runs.

The middle case: a static method that takes the object

Sometimes a helper needs data from one object, but you are tempted to make it static and pass the object in as a parameter. In Python, a regular method can be called in that way too. The line below calls the regular total method through the class name and passes the cart as self:

print(Cart.total(cart))

It prints the same 12.5 as cart.total(). Python passes the object as self in both cases, so the two calls are the same call written two ways. A reader has to notice that. Write cart.total(), which says what it means, and keep static methods for helpers that are useful without any cart, such as sum_prices.

A quick test

When you cannot decide, ask two questions:

  1. Does the method use any attribute of the object, or call any other method that does? If yes, it must be a regular method, with self as its first parameter.
  2. Could I explain what it does with only its inputs, without saying which object it belongs to? If yes, it can be a plain function or a static method.

If the answer to both is yes, the function is a plain helper, and making it static or leaving it at the top level is correct. If the answer to the first question is yes, make it a regular method, whatever the second question says.

Here is the test applied to four methods of a bank account class. deposit(amount) adds to the balance of one account, so it is a regular method. withdraw(amount) reads and changes the same balance, so it is a regular method too. is_valid_amount(amount) checks only that the number is above zero, so it is static. interest_for(balance, rate) multiplies two inputs, so it is static as well. The last two can be used before any account exists, for example to check a form field or to show a preview of the interest before the customer opens an account.

Static helpers also make testing simpler. A helper with no stored data gives the same answer for the same inputs, so a test can call it with a value and check the result, without building an object first. The lesson on pure functions, earlier in this module, covers this idea in more detail.

Worked example: a grading helper

Here is a slightly larger example. A Gradebook stores scores for one class. Turning a single score into a letter needs only the score, so it is a static method. Working out the class average needs the stored scores, so it is a regular method.

Python · editable

Output:

Average: 74.33333333333333
Letters: A B C

The average is the sum of 91, 74 and 58, which is 223, divided by 3. The / operator always gives a float, so the result has a fraction without any extra step. The operator // does a different job. It divides and drops the fraction, so 223 // 3 gives 74. Use / for an average.

Python has no separate type for a single character. letter_for returns a one-character string, such as "A".

Common mistakes

Calling a regular method through the class name

A regular method needs an object to supply self. Calling it through the class leaves self empty:

class Cart2:
    def total(self):
        return 0.0
 
print(Cart2.total())

Python stops with this error:

TypeError: Cart2.total() missing 1 required positional argument: 'self'

The fix is to create an object first: Cart2().total(). Or, if the method really does not need the object, add @staticmethod and remove self.

Forgetting self in a regular method

The opposite mistake is a method inside a class that does not list self:

class Cart:
    def total():
        return 0.0
 
cart = Cart()
print(cart.total())

Python passes the object as the first argument even when the method has no parameter for it, so the call fails:

TypeError: Cart.total() takes 0 positional arguments but 1 was given

The fix is to add self as the first parameter: def total(self):.

Reading self in a static method

The class Two shown earlier fails because read_count is static and has no self. The error is NameError: name 'self' is not defined. The fix is to remove @staticmethod and add self as the first parameter, so the method becomes a regular method.

Sharing a list between objects by accident

A name that is set at the top of a class, outside any method, belongs to the class itself. Every object shares it. This is the Python version of a static field.

Python · editable

Output:

[1]

The second bag received the item, although only b1 was asked to add it. Both objects share one list, because items belongs to the class. The fix is to create the list for each object inside __init__:

class Bag:
    def __init__(self):
        self.items = []
 
    def add(self, x):
        self.items.append(x)

Putting every helper in a class with only static methods

A class that holds only @staticmethod functions works, but it is usually a module in disguise. The module from the earlier section is shorter and easier to read. Use a class when the helpers really belong to a type of object, as sum_prices belongs to Cart.

Expecting a private constructor

Python has no private constructor. Nothing stops TextUtils() from creating an empty object, and that object would do nothing useful. A module avoids the question, because it is never built into an object.

Practice

Each exercise has its answer below it. Try to write your answer before you read it.

Exercise 1

Write a plain function is_even(n) that returns True when n is even. Then call it with the number 7 and print the result.

Show the answer

Python · editable

Output: False. The function uses only its input, so it needs no object and no class.

Exercise 2

A class Counter has an attribute count and a regular method increment(self) that adds 1 to count. Should a method reset(self) that sets count to 0 be static or a regular method?

Show the answer

It should be a regular method. reset changes count, and count belongs to one particular counter. A static reset would have no counter to change.

Exercise 3

The following code does not work. Explain the error and fix it.

class Person:
    def __init__(self):
        self.name = "Sam"
 
    @staticmethod
    def get_name():
        return name

Show the answer

Python stops with NameError: name 'name' is not defined. The attribute name belongs to each Person object, and get_name is static, so it has no object to read from. Fix it by removing the decorator and adding self:

def get_name(self):
    return self.name

Callers then write person.get_name().

Exercise 4

Write a function average(values) that returns the average of a list of numbers. Decide whether it should be a plain function or a static method, and explain your choice in one sentence. Then show what it prints for [1, 2, 4] and for an empty list.

Show the answer

It should be a plain function, because it uses only its input list and reads no attribute. Code:

Python · editable

Output: 2.3333333333333335. For an empty list, len(values) is 0, so the division fails with ZeroDivisionError: division by zero. A careful version checks for an empty list first and returns 0.0, as Gradebook.average does.

Exercise 5

A Playlist class stores song titles in a list. Write a regular method song_count(self) that returns how many songs it holds, and show the calls after creating one playlist and adding two songs.

Show the answer

Python · editable

Output: 2. The count belongs to one playlist, so the method is a regular method.

Summary

  • A plain function at the top level of a file needs no object, and you call it by its name, such as square(6).
  • A module is one .py file. Import it to use its functions, and write the module name in front of each call.
  • A static method, marked with @staticmethod, sits inside a class and has no self, so it cannot read an object's attributes.
  • If a helper reads or changes one object's attributes, make it a regular method, so the call reads as cart.total().
  • The errors missing 1 required positional argument: 'self' and name 'self' is not defined mean the code gives a method the wrong kind of access to an object.
Practice this topic: coding problems for Functions and Scope →

Stuck? Ask the tutor

Ask about anything in this lesson. Answers are short and use simple words.

Sign in to ask the tutor.

← Pure functions in Python