Consider the following function:

import math

def xy_distance(coord):
    """Calculates an Euclidean distance between the origin and the given coordinate."""
    if isinstance(coord, (str, int, float)):
        return float(coord)
    if isinstance(coord, (tuple, list)):
        return math.hypot(float(coord[0]), float(coord[1]))
    if isinstance(coord, dict):
        return math.hypot(float(coord["x"]), float(coord["y"]))

As far as we understand the docstring, this function does calculate an Euclidean distance. However, as seen in the implementation of this function, it involves many unrelated processes and constraints:

We may have seen this kind of functions many times, especially in typeless Python programs. Although the real objective of the function is simple, users will easily get confused to use this function due to unclear explanation of coord. Users may also get confused to guess the meaning of code around invocations of this function, because the code would tell us nothing about the underlying behavior. For example:

import json

with open(path) as fp:
    data = json.load(fp)

# We can't guess what kind of values are communicated through `data`,
# but somehow the following invocation "works" without any errors,
# but there is no guarantee of it.
distance = xy_distance(data)

This kind of code easily becomes a source of complicated bugs that will be suddenly revealed by unrelated changes of other parts of code.

If the function looks to behave “intelligently,” it is a signal of wrong cohesion. We basically need to take care of keeping every function trivial to avoid unnecessary complexity of the codebase.

One of the simple ways to detect such “intelligence” is annotating full type information to the function. If the annotation involves many types, e.g., Any, or unions of unrelated types (str and int/float in this case), we need to notice that the function actually does too many things.

def xy_distance(
    # Too many types allowed for `coord`
    coord: str | int | float | tuple[Any, Any] | list[Any] | dict[str, Any],
) -> float:
    ...

Refactoring this kind of functions is simple: “let each function do no more than one thing.” Especially, if the function takes arguments with union types, consider a possibility to decompose such argument, e.g., separating union to multiple arguments/functions, or simply avoid unnecessary types.

In our case, xy_distance should calculate only the Euclidean distance from given two values (or maybe a 2-tuple), and let users process required checking and/or conversion of values by themselves.

def xy_distance(coord: tuple[float, float]) -> float:
    """Calculates Euclidean distance on the XY plane."""
    # No ambiguity of underlying value of `coord`.
    return math.hypot(coord[0], coord[1])

# TypeGuard is available from Python 3.10.
def is_xy_coord(obj: Any) -> TypeGuard[tuple[float, float]]:
    """Type guard to check if the object can be treated as (x, y)."""
    return (
        isinstance(obj, tuple)
        and len(obj) == 2
        and all(isinstance(x, float) for x in obj)
    )

with open(path) as fp:
    data = json.load(fp)

# If necessary, let users check if the input has a correct data.
if is_xy_coord(data):
    # `data` should have the type tuple[float, float].
    distance = xy_distance(data)
else:
    raise ValueError("Invalid data")

The code looks a bit redundant compared with the first example in spite of very limited use-case (it works only when we got tuple[float, float]). However, most lines of code involves no ambiguity about underlying data, and we can basically believe that the inner function xy_distance does never raise any errors so long as we follow its correct typing.