Embercode
✕
Functions and ScopeLesson 3 of 17

35 min read

One name, several inputs in Python

Why it matters

Many actions apply to more than one kind of input. You can add two whole numbers, add two decimals, or add three numbers, and in each case the idea is "add". Without a way to share one name you would have to invent names such as add_ints, add_floats and add_three, and the reader would have to remember which one to use. Many languages solve this with overloading, where several functions share a name and differ in their parameters. Java does this. Python does not, and the reason is worth knowing. Python has three tools that cover most of the same need, and this lesson teaches each one.

The idea: one name, several versions

In Java, two methods with the same name and different parameter lists are two separate methods, and the compiler picks one for each call. Python does not work that way. A function is an object that is stored under a name. When Python reads a second def with the same name, it replaces the first one. The earlier function is gone.

Python · editable

Output:

21
Traceback (most recent call last):
  File "/path/to/main.py", line 8, in <module>
    print(area(5))
          ~~~~^^^
TypeError: area() missing 1 required positional argument: 'height'

The first call works, because the second def has already replaced the one-parameter version. The second call fails, because the only area left takes two parameters. Python gives no warning when the name is reused. You see the problem only when a call with the wrong number of arguments runs.

So Python programmers do not overload. They use one of the three tools below, depending on the problem.

Tool 1: optional parameters

If one function can handle several cases, give the extra parameters a default value. The caller then passes only what is needed. This is the most common tool, and you saw it in the parameters lesson.

Python · editable

Output:

25
21

The default value None means "not given". The test height is None checks whether the caller left the argument out. The word is checks identity, meaning the value is the same object as None. For None this is the usual check, because None exists only once in a program.

Optional parameters work well when the variations differ in one small part. When the variations are very different, a single function with many optional parameters becomes hard to read, and the next tool is better.

Tool 2: one function that checks its type

A function can look at the type of its argument and act on it. The built-in isinstance checks whether a value is of a given type.

Python · editable

Output:

whole number 42
decimal number 2.5
text "hello"
yes
something else: None

The order of the tests matters here. A bool is a kind of int in Python. isinstance(True, int) is True, so the bool test must come first. If the int test came first, describe(True) would print whole number True. Check the order whenever one type is a special case of another.

The !r in the last line is a conversion flag. It shows the value the way Python would write it in code, so None prints as None and text would print with quotes. The f-string form {value!r} is handy for debugging.

This tool keeps every case in one place, which is its strength. Its weakness is that every new case means editing the same function. For a small set of cases that is fine. For a set that grows, use the third tool.

Tool 3: singledispatch

The standard library module functools has a decorator called singledispatch. A decorator is a line starting with @ that changes the function below it. singledispatch turns a function into one that picks a version by the type of its first argument. You write a base version for unknown types, and register a version for each type you care about.

Python · editable

Output:

whole number 42
decimal number 2.5
text "hello"
yes
text "c"
something else: None

Each registered version is a function with a different name, written as _. The name does not matter, because register stores the function under its type. The : int annotation is the type hint that tells register which type the version handles. A type hint is a note about the expected type. Python does not enforce it while the program runs, but singledispatch reads it when you register.

Look at describe(True). The bool version is registered, and bool is a more specific type than int, so singledispatch chooses it. This is the same idea as Java's rule of choosing the most specific version. The rule is applied at run time, using the actual type of the value.

Look at describe("c"). Python has no character type. A single letter in quotes is a string of length one, so it goes to the str version and prints as text. A Java programmer may expect a character code here, as in the Java lesson on overloading. In Python there is no such trap, because the type is str from the start.

singledispatch only looks at the first argument. If your choice depends on two arguments, you need a plain function with isinstance checks, which is the second tool.

Choosing a tool

Use optional parameters when the variations differ in one small part, and the default is what most callers want. Use an isinstance function when a few types need different handling and the list is short. Use singledispatch when the same action has many type-specific versions, or when other people may add a new type later without editing your function.

The three tools are not exclusive. A singledispatch version may use optional parameters inside it, and a plain function may dispatch on types with isinstance and call helper functions.

A worked example: a fee calculator

Here is a small program that shows the optional-parameter tool in a realistic case. A shop charges a fee on a bill. Most bills use the standard rate, and the caller can pass a different rate for a special customer.

Python · editable

Output:

2.0
5.0
1.0
0.5

Trace fee(10). The rate is 0.02, so the result is 0.2. That is below the minimum of 1.0, so the function returns 1.0. Trace fee(10, minimum=0.5). The result is still 0.2, which is below the new minimum of 0.5, so the function returns 0.5.

The keyword form rate=0.05 is a named argument. It makes each call read clearly, and the order of the optional arguments no longer matters.

Common mistakes

Defining the same function name twice expecting both to exist. The second definition replaces the first, and the earlier one can no longer be called. Use a different name, a default parameter, or singledispatch.

Testing isinstance(x, int) before isinstance(x, bool). Every bool is also an int, so the bool case never runs. Test the more specific type first.

Using == to test for None. The usual check is value is None. Both work for None in most code, but is states the intent.

Forgetting that singledispatch looks only at the first argument. A call with the second argument as the deciding type picks the wrong version or raises an error. Use isinstance for multi-argument choices.

Missing the type hint in a register version. Without the annotation, singledispatch cannot tell which type the version handles, and it raises a TypeError when you register it. The message names the problem, so read it and add the annotation.

Variations and edge cases

A function may accept any number of positional arguments with *args. Inside the function, args is a tuple. The varargs lesson covers this in detail. It is the closest Python idea to Java's varargs.

Two classes can each have a method with the same name, and a subclass can replace a method its parent has. The object lessons show how Python chooses between them at run time. For now, the three tools above cover the need.

Interview-style questions

An interviewer may ask, "Does Python support overloading?" A good answer says no, explains that a second definition replaces the first, and then names the tools that give the same effect: optional parameters, type checks, and singledispatch.

A second question is, "What does describe(True) print if int is checked before bool?" The answer is the int text, because True is an int. The fix is to test bool first.

Real-world uses

The standard library uses these tools widely. functools.singledispatch appears in code that serialises objects to different formats, where each type needs its own conversion. Optional parameters appear in almost every standard function, such as sorted(items, reverse=True), where reverse is an optional named argument. Type checks with isinstance appear in validation code, where a function must refuse or convert unexpected input.

Practice

  1. Write total(a, b=0) that returns the sum of a and b. Call it with one argument and with two, and predict both results.

Show the answer

def total(a, b=0):
    return a + b
 
print(total(5))
print(total(5, 3))

The output is 5 and then 8.

  1. Write a function describe with isinstance checks for int, str and list. Put a list of your own choice in the test and predict the output.

Show the answer

def describe(value):
    if isinstance(value, int):
        return f"number {value}"
    if isinstance(value, str):
        return f"text {value}"
    if isinstance(value, list):
        return f"list of {len(value)} items"
    return "unknown"
 
print(describe(3))
print(describe("hi"))
print(describe([1, 2, 3]))

The output is number 3, text hi and list of 3 items.

  1. Explain what describe(True) prints in the isinstance version if the int check is moved above the bool check, and why.

Show the answer

The output is whole number True if the int check runs first, instead of yes. True is an int, so the int test is true. The bool test must come first.

  1. Write a function greet(name, greeting="Hello") and call it with one argument and with a named argument for greeting. Predict both lines.

Show the answer

def greet(name, greeting="Hello"):
    print(f"{greeting}, {name}!")
 
greet("Asha")
greet("Asha", greeting="Namaste")

The output is Hello, Asha! and then Namaste, Asha!.

  1. Use singledispatch to write size(value) that returns len(value) for a string or a list, and returns 1 for any other value. Test it with "abc", [1, 2] and 7.

Show the answer

from functools import singledispatch
 
@singledispatch
def size(value):
    return 1
 
@size.register
def _(value: str):
    return len(value)
 
@size.register
def _(value: list):
    return len(value)
 
print(size("abc"))
print(size([1, 2]))
print(size(7))

The output is 3, 2 and 1.

  1. Predict the output of this program, then check it. Explain what happens to the first definition.
def show(x):
    print("first", x)
 
def show(x, y):
    print("second", x, y)
 
show(1, 2)
show(3)

Show the answer

second 1 2

The second line raises TypeError: show() missing 1 required positional argument: 'y'. The second def replaced the first, so the one-argument call has no matching function.

  1. Write fee(amount, rate=0.02). Call it as fee(100) and as fee(100, 0.05). Predict both results.

Show the answer

def fee(amount, rate=0.02):
    return amount * rate
 
print(fee(100))
print(fee(100, 0.05))

The output is 2.0 and then 5.0. The * gives a decimal here because rate is a float.

What you now know

  • Python does not overload functions. A second def with the same name replaces the first.
  • Optional parameters with default values cover many small variations of one action.
  • isinstance checks inside one function handle a short list of types. Test the more specific type first.
  • functools.singledispatch picks a registered version by the type of the first argument, and it needs a type hint on each registered version.
  • A str of length one is text, not a character code, so the Java overloading trap does not apply.
  • The error to recognise is TypeError: ... missing 1 required positional argument, which often means a name was reused.
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.

← Parameters and return values in Python