3

What would be the most pythonic way to pass an object type as an agrgument in a function?

Let me give you an example. Let's say I was trying to get a configuration from an environment variable. Because all environment variables are strings I need to cast the value to the correct type.

To do this I need to tell the function the desired type. This is the purpose of the coerce argument. My first instinct is to pass in the desired type as the value for coerce. However, I am not sure if there are any implications or problems in doings so.

import os

# The Function
def get_config(config: str, coerce: type, default: any, delimiter: str = ","):
    value = os.getenv(config, None)  # Get config from environment
    if value is None:
        return default  # Return default if config is None

    if coerce is bool:
        value = str2bool(value)  # Cast config to bool
    elif coerce is int:
        value = str2int(value)  # Cast config to int
    elif coerce is list:
        value = value.split(delimiter)  # Split string into list on delimiter

    return value  # Return the config value

# Usage
os.environ["TEST_VAR"] = "True"

test_var = get_config("TEST_VAR", bool, False)

print(test_var)  #  output is True
print(type(test_var))  # output is <class 'bool'>

To me this seems more clear and pythonic than using a string such as "str" or "bool" to specify the type. However, I would like to know if there could be any problems caused by passing around built in types as function arguments.

2
  • 1
    this is fine. do you have any specific concerns? This is probably a better fit for CodeReview not StackOverflow Commented Oct 12, 2019 at 18:41
  • @juanpa.arrivillaga No specific concern. Just not sure if this will come back to bite me later. Thank you for pointing out CodeReview I did not know it existed! Commented Oct 12, 2019 at 20:46

3 Answers 3

2

You can simplify the code and make it more powerful by just directly passing the conversion function (type annotations are left as an exercise):

def get_config(config, convert, default):
    value = os.getenv(config, None)
    return default if value is None else convert(value)

test_var = get_config("TEST_VAR", str2bool, False)

and perhaps having a helper function for the list case:

def make_str2list(delimiter=','):
    return lambda s: s.split(delimiter)

test_var = get_config("TEST_VAR", make_str2list(':'), [])
Sign up to request clarification or add additional context in comments.

1 Comment

yup. plus, you’re already getting some builtins like str, int. casting strings via str does nothing, but it generalizes the concept
2

Since all you are doing with the type argument is to compare it one by one to certain specific types that you're expecting, rather than actually using type to construct objects of that type, it is doing nothing different from passing in a string such as 'str' or 'bool' as an argument and compare it to several string constants.

Instead, you can make the conversion functions such as str2bool and str2int themselves an argument, so that you can call coerce(value) to convert value in a generic way. Store such conversion functions as attributes of a dedicated class for better readability, as demonstrated below:

import os
import typing

class to_type:
    bool = 'True'.__eq__
    int = int
    list = lambda s: s.split(',')

def get_config(config: str, coerce: typing.Callable = lambda s: s, default: any = None):
    value = os.getenv(config, None)
    if value is None:
        return default
    return coerce(value)

os.environ["TEST_VAR"] = "True"
print(get_config("TEST_VAR", to_type.bool))
os.environ["TEST_VAR"] = "2"
print(get_config("TEST_VAR", to_type.int))
os.environ["TEST_VAR"] = "a,b,c"
print(get_config("TEST_VAR", to_type.list))
os.environ["TEST_VAR"] = "foobar"
print(get_config("TEST_VAR"))

This outputs:

True
2
['a', 'b', 'c']
foobar

Comments

1

In this specific instance, I'd argue that coerce should not be a type, but rather an Enum: both because get_config only supports a small set of possible values, and because it doesn't use the type values directly in its handling. If nothing else, the function signature is more precise with an Enum.

ConfigType = Enum('ConfigType', 'STR BOOL INT LIST')

def get_config(config: str, coerce: ConfigType, default: any, delimiter: str = ","):
    value = os.getenv(config, None)  # Get config from environment
    if value is None:
        return default  # Return default if config is None

    if coerce is ConfigType.BOOL:
        value = str2bool(value)  # Cast config to bool
    elif coerce is ConfigType.INT:
        value = str2int(value)  # Cast config to int
    elif coerce is ConfigType.LIST:
        value = value.split(delimiter)  # Split string into list on delimiter

    return value  # Return the config value

That said, if you really wanted to use type then Python 3.8 (which should be released in two days' time) supports literal types, meaning you could declare the function as follows:

def get_config(config: str, coerce: Literal[str, bool, int, list], default: any, delimiter: str = ","):

Comments

Start asking to get answers

Find the answer to your question by asking.

Ask question

Explore related questions

See similar questions with these tags.