Parameter hints

Functions calls with positional-only arguments can get quite unreadable:

def calc_final_grade(assignment, project, midterm, exam, participation, /):
    return (
        .20 * assignment
        + .25 * project
        + .20 * midterm
        + .25 * exam
        + .10 * participation
    )

final_grade = calc_final_grade(85, 90, 75, 88, 95)
print(f"The final grade is: {final_grade:g}")

Some IDE’s support inlay hints, but these can get cut off and make line length hard to measure:

Could we add a notation to hint the parameter names, but which is ignored at runtime?

final_grade = calc_final_grade(assignment:85, project:90, midterm:75, exam:88,
                               itdoesntmatterwhatyouwritehere:95)
final_grade = calc_final_grade(assignment=85, project=90, midterm=75, exam=88,
                               itdoesntmatterwhatyouwritehere=95, /)
final_grade = calc_final_grade('assignment'=85, 'project'=90, 'midterm'=75,
                               'exam'=88, 'itdoesntmatterwhatyouwritehere'=95)

Edit: with 6+ people against, I won’t follow through with this.

Why use positional-only arguments then? They’re not the default.

5 Likes

This is just an example, this mostly applies to built-in functions such as range().
Although I also use it when the signature of a class is incompatible with its super class. e.g.

class Representation(str):
    def __new__(cls, obj: object = "", /) -> Representation: ...

Also, using parameter hints would be faster than keyword arguments:

$ python -m timeit -s "f = lambda a, b, c: a + b + c" "f(1, 2, 3)" && python -m timeit -s "f = lambda a, b, c: a + b + c" "f(a=1, b=2, c=3)"
5000000 loops, best of 5: 69.5 nsec per loop # positional
5000000 loops, best of 5: 99.2 nsec per loop # keyword

Yes, I know this is pre-mature optimisation, but it would be as readable as keywords.

I think this is a needless complication to work around a poor API design.

12 Likes

Something like this?

final_grade = calc_final_grade(
    85, # assignment 
    90, # project
    75, # midterm 
    88, # exam
    95, # itdoesntmatterwhatyouwritehere
)
2 Likes

You can do

final_grade = calc_final_grade(
    85, # assignment
    90, # project
    75, # midterm
    88, # exam
    95, # participation
)
1 Like

Yes, both, I would put them on separate lines anyway when they don’t fit on a single line:

final_grade = calc_final_grade(
    assignment:85,
    project:90,
    midterm:75,
    exam:88,
    itdoesntmatterwhatyouwritehere:95
)

But this is overkill for functions like range(), and it makes more sense if the parameter name comes first.

I see 6 people disagree with me. OK, then not.

If you find that someone else’s interface is awkward because it uses positional-only arguments, make a wrapper.

For Python code, you can probably use the inspect module to automate this. It’s not trivial, though.

The problem is that built-ins don’t actually have names for those positional-only arguments, because they’re implemented in a context that doesn’t need names - the C code receives the arguments via the C API which has already stuffed them into a tuple, and assuming those arguments are unpacked into local variables in the C code, those names aren’t exposed anywhere (even in a debug build, they’d only be visible to a C debugger). So we can’t support something like range(step=2) “automatically”, because range doesn’t just reject passing the arguments by keyword - it doesn’t have any keywords to use.

I don’t really understand why. That doesn’t seem like a good way to signal the issue. But aside from that, __init__ and __new__ are special, because they’re concerned with object creation. Beyond that point, methods should uphold the Liskov Substitution Principle.

2 Likes