Add a safer version of cast?

Consider this code:

    def f(interest: float) -> float:
        return sum(ci * interest ** (i + percent_into_year)
                   for i, ci in enumerate(reversed(contribution_limits)))

Unfortunately, type checkers think that float ** float is Any. (See the typeshed for why.)

MyPy warns me:

error: Returning Any from function declared to return "float"  [no-any-return]
            return sum(ci * interest ** (i + percent_into_year)

To solve the problem, I could:

  • Add a type: ignore, or
  • cast the expression to float.

What about a safer version of cast:

    def f(interest: float) -> float:
        return sum(ci * safe_cast(float, interest ** (i + percent_into_year))
                   for i, ci in enumerate(reversed(contribution_limits)))

Or perhaps instead of as_instance(x, T) or cast(T, x, verify=True) (which would only work for certain non-generic types?)

Either way, this would verify that isinstance(x, T), and then return x. It’s like a safer version of cast.

Can you cast the expression to float, and also add:

assert percent_into_year >= 0

Yes, but that still has the problem that you’ve moved some of the verification onto yourself.

Also, I think this is a more general problem

Doesn’t assert isinstance(x, T) work? I thought type checkers could infer from such an assertion that the type of x is now T.

I tried to test this, and neither mypy nor pyright reported the error you describe. I used both your code (which uses a couple of undefined variables, so I guessed) and

def f(x: float, y: float) -> float:
    return x ** y

Can you provide a reproducible example of getting this error?

1 Like

Why not just define it yourself[1]?

def as_instance[T](x: object, type: type[T]) -> T:
    assert isinstance(x, type)
    return x

But as others have pointed out in most cases you can just do the assertion inline, it’s mostly comprehensions where you may want to cast a subexpression.

  1. since you only seem to care about cases where isinstance works this should be powerful enough ↩︎

I tried to test this, and neither mypy nor pyright reported the error you describe.

You need to enable “warn_return_any = true” for MyPy to warn on this. Anyway, even if you have this off, having this be untyped may be undesirable for you (if, for example, you want other things to be checked).

Yes, you could do:

def safe_power(x: float, y: float) -> float:
    retval = x ** y
    assert isinstance(retval, float)
    return retval

and then use it in place of the original power in f. Is that your preferred solution?

Right, you can absolutely build this yourself. My proposal is to add this to typing. By the way, I would prefer the second argument to a type-form rather than a type to ensure that you can pass in unions, etc.

I think my issue is that I can’t do it inline. I need to break out a special function.

It’s certainly perfectly adequate IMO. Or write your own as_instance as @Daverball suggested. I definitely wouldn’t bother looking for a stdlib function to do this.

1 Like

I don’t think mypy’s no-return-any is serving you (or other users) well here. The coercion to float (type system level coercion) happens due to your return annotation. You don’t have a better type from the standard library available here due to limitations in how the numerics were and are currently handled, and mypy’s no-return-any is breaking the gradual guarantee that you can replace any correct annotation with Any and not receive new errors.

While I’m sympathetic to fixing the numeric situation and reducing the number of places the standard library needs to return Any, I don’t think what you have here is a good argument for a different cast behavior. cast is always exactly as safe as your ability to ensure the correct invariants are upheld.

The choice to type this as Any rather than float | complex may hide false positives, but it also hides actual possible bugs, If we pretend for a moment that this was typed as float | complex, casting if you know this is always a positive power, or checking to ensure if it isn’t would be correct here if this were typed accurately. If you’re reintroducing a need to check by breaking the gradual guarantee, why would the same checks you would be otherwise required to do be inadequate here?

Fair enough, I’ll use one of those solutions for now. This problem has come up for me at least a few times. I thought I would propose the idea to see if other people have run into it too.

We could add this function to GitHub - hauntsaninja/useful_types: Useful types for Python.

1 Like

I’m not sure if safer is the right word, but I’d like a check:bool=False option for typing.cast to do a runtime check for the type of the instance.

I’ve had cases where people use cast(..) to get mypy to agree (incorrectly) when that assert on type would have caught the wrong type going in.

I don’t usually care for asserts in production code, but raising something, optionally, during the cast would be nice.

Having a default of false keeps behavior the same for all current users.

Yes, but that still has the problem that you’ve moved some of the verification onto yourself

So what? Static typing doesn’t remove the programmer’s obligation to sanitise untrusted inputs.

Without that assert or something else, Mypy can’t otherwise assume that percent_into_year is not negative

The typeshed comment is a little incorrect. The thing we’d be concerned about being negative is interest here. And interestingly, negative interest rates are a real thing, so it’s not actually correct to just assume this without specific additional information. Handling negative interest rates using complex would also be incorrect to how these function in the real world. Hence, it’s a bit of a lose-lose here that python automatically treats powers that would result in a complex as a complex without user opt-in. You’d arrive at the wrong result, and the type checker still couldn’t warn you of it because it was deemed too noisy to type this correctly in the type shed.

My apologies. You’re quite right. I should’ve been asserting interest >= 0.

@NeilGirdhar I can actually see safe_cast or strict_cast would be quite useful in other circumstances. It would allow telling the typechecker what you want it to check explicitly.

But in cases such as in your example, where the bug is due to specific values of an argument (e.g. negative ones) not its general type, then I think the code is more readable and easier to understand if that case (negative values) is explicitly tested for.

The proposed function is an assertion.

And even with your assertion, MyPy won’t be able to figure out that the return type is float.

If you add the assertion on the value, and then add a cast or type: ignore to stifle the error, you will still need to add a comment explaining why the cast is okay. Something like:

# Ensure that the result of power is float so that we can ignore the type error.
assert percent_into_year >= 0

Even with the comment, it’s not great since it still relies on ensuring that the assertion matches the code and the type error can really be suppressed.

Whereas with safe_cast(float, ...), you are ensuring what you want (that the type is float) directly, and you don’t need a comment. In general, good code reflects intentions. This makes it self-commenting.

On top of that, if the code changes, the assertion keeps getting run and if something goes wrong, it should catch. It does not require vigilance. In general, it’s better to do instance checks than to suppress type errors.

I disagree. I find an inequality assertion before a mathematical calculation, both using elementary Python syntax to be not only sufficient but self explanatory.

Furthermore, the existing basis, and your idea are called cast and safe_cast respectively, but they are not true type casts. Python is still a dynamic language. If your intention is to have a true type cast, then why not just call float on it?

I’m fortunate that I have experience of lots of different programming languages, and know what type casting is. But if I didn’t know that (like the majoriiy of Python users), I’d far prefer the unambiguous maths, to having to search the docs to find the implementation details about whatever the latest sparkling new feature of the type system is. Especially if its name misleads the reader about what it actually is.

Asserting values is not sufficient to justify the cast that you need on the next line. So you either need a comment or an instance check. If the code were imperative, you would have a regular instance-check. This is the comprehension version of that.

The intention isn’t to do a type cast. The intention is to suppress the type error safely.

I suspect typing.cast was so-named because it type casts the type form that is associated with an expression.

The intention isn’t to do a type cast. The intention is to suppress the type error safely.

In that case cast and safe_cast are bad names for their features.

What are you using floats for financial calculations for anyway? By definition, floats are prone to Floating point error. Isn’t best practise to use an int of the smallest denomination (e.g. pennies) or a Decimal type to some fixed precision?

You shouldn’t assert on the value, you should assert the type. You’re saying “I am sure this will be a float”, which is what an assertion is. You can add a comment by the assert saying “this is safe because percent_into_year >= 0”. I’m not even clear why you think assert isinstance(..., float) is any less clear than safe_cast(float, ...) - the two seem essentially identical to me.

If you want to check the value then you should either do a proper runtime check, with an actual error (not an assertion failure), or you should include an additional assert, whose purpose is to ensure that the caller is adhering to the API contract. That assert is independent of the type check, except in that it validates the conditions needed to ensure the type assert is safe.

If none of the above match your views on how to handle this situation, that’s fine. You can write your own safe_cast, as we’ve seen. But with so many existing ways of handling this situation, adding yet another approach to the standard library seems unnecessary.

I’m glad you agree with me! Asserting on the type is exactly what I suggested as being superior than asserting on the value:

Where do you get the impression I said that one is less clear than the other? This is what I said:

I never proposed checking the value. Although I have to disagree with you about raising errors in cases where you’re certain that something won’t happen. I think assertions are the correct approach for that case.

The point of safe_cast is to have an inline version of isinstance since your solution would require defining a new function.