From a conceptual point of view, you might want to switch the order of the functions. Instead of square using check_type to check its arguments, you can make check_type accept square as the callback to call with sanitized arguments if everything passes.
A naive approach might be as follows:
def check_type(x, hook):
return hook(x) if type(x) in [int, float] else None
def square(x):
return x**2
print(check_type(2, square))
# 4
print(check_type('2', square))
# None
This is not super elegant, but it is a design improvement over your original approach in a number of ways. For one thing, the function square is free to focus on doing it's job instead of worrying about sanitizing arguments, which helps encapsulation. For another, it makes type checking more consistent across functions, because you can do it in one place instead of having to remember to do it in each function.
As far as elegance goes, python provides a mechanism for doing this quite cleanly, called decorators. A decorator is a function that accepts a function or class as input, and returns a replacement object. This is usually used to wrap functions with extra functionally, exactly like you are trying to do.
A first pass decorator implementation would look like this:
from functools import wraps
def check_type(func):
@wraps(f)
def wrapper(x):
return func(x) if type(x) in [int, float] else None
return wrapper
@check_type
def square(x):
return x**2
print(square(2))
# 4
print(square('2'))
# None
Notice that the wrapper function that the decorator returns to replace square is itself decorated. functools.wraps modifies the function object by updating the name and other attributes to make wrapper be harder to distinguish from the original square.
On an unrelated note, I probably wouldn't allow my function to return a value on inappropriate input. When you return None, you are forcing the caller to check for None vs a valid value, effectively defeating the purpose of having a type checker in the first place. Instead, an exception is a better design choice because it doesn't punt the responsibility down the line:
def check_type(func):
@wraps(f)
def wrapper(x):
if type(x) in [int, float]
raise TypeError ('Expected one thing but got the other')
return func(x)
return wrapper
As a final point, you may want to reconsider how you do type checking, or whether you do it at all. For example, you can extend most of the built-in classes. If you want to allow such extensions, a better phrasing might be
if isinstance(x, (int, float)):
Or you can use abstract base classes registered in the numbers module. This will allow you to handle things like numpy scalars, decimals, etc:
if isinstance(x, numbers.Number):
Finally, you may want to consider doing the most pythonic thing, and passing through anything that supports whatever operation you need. That means not doing any type checking at all. If you pass an instance of something with no __pow__ method, the interpreter will raise a TypeError for you in square.