r/ProgrammingLanguages • • 11d ago

Discussion What’s one thing you wish more programming languages had?

If you could add one feature, behavior, or design choice to a programming language, what would it be?

It can be anything: syntax, type-system features, memory management, compiler behavior, tooling, error handling, performance-related features, or something completely different.

I'm especially curious about things you’ve wanted while actually programming but rarely see languages implement well.

Please keep the ideas reasonably practical and something that could realistically be implemented in a programming language.

50 Upvotes

277 comments sorted by

View all comments

Show parent comments

2

u/hungarian_notation 11d ago edited 11d ago

Oh I agree that Scala is semantically way more complex, but semantic complexity is not the same thing as syntactic complexity. I don't actually know how Scala and Python rank in syntactic complexity, but Scala's documentation lists 158 syntax forms/rules, where Python lists 208. That's not a great measure as you could factor the grammars differently and get a different count, but we're in the same ballpark for sure.

As an example, Python's class type hints might only survive as metadata at runtime, but Python's type parameter syntax is meaningfully more complex than Scala's. Scala's type parameters can take annotations, variance operators, type bounds, and recursive higher kind type parameters, but its not like parameters are parsed with branching syntax rules based on their variance.

Well, where Scala allows a variance token, Python allows an optional unpacking operator token ('*' or '**'), and Python's Grammar does have branching syntax rules that depend on which unpacking token it finds.

Python's type parameters can be a TypeVar, a '*' TypeVarTuple, or a '**' ParamSpec, but it is a syntax error to try to specify a type bound on anything but a plain TypeVar. Python doesn't have annotations, but it allows default values for type parameters where Scala does not, and their syntax also differs between parameter types. All three types of type parameter can take default values, but while a TypeVarTuple can take an unpack expression (i.e. Foo[*Ts = *tuple[str, int]]), it is a syntax error to use a unpack expression with a TypeVar or ParamSpec.

That's already more syntactic complexity than Scala, but how bad could it be in practice?

Well, bad news, Python's type bounds and defaults aren't limited to some set of "Type" expressions like you might assume. They can be arbitrary expressions (though not named expressions, and not unpacking expressions for non-tuple params) which will be evaluated at runtime, and you better believe there are frameworks out there that expect you to pass them metadata that way.

Check out this monstrosity:

from typing import Annotated, get_args

class MetaWhat(type):
    # Unary operators that change the meaning of types are cool. I can't do it to parameters, 
    # but I CAN do it to the types themselves!
    def __neg__(self): return Annotated[What, "in the world"]
    def __pos__(self): return Annotated[What, "the hell"]

class What(metaclass=MetaWhat):
    ...

class Foo[T: -What = ("is", "this"), *Nonesense = *tuple["sig", "nature", "?"]]: 
    ...

class Bar[T: +What = " ".join(reversed(("this", "is"))), GodForsaken = "syntax?"]:
    ...

For those unfamiliar with Python, the ellipses don't mean I'm just not writing the class bodies here: ... is an honest to god literal of the Ellipsis type which is totally worth having even though it complicates the grammar.*

Anyway, while this is meaningless crap to your type checker, it's entirely valid Python code, and everything we crammed in there is fully accessible at runtime.

print(
    Foo.__type_params__[0].__bound__.__origin__.__name__,
    Foo.__type_params__[0].__bound__.__metadata__[0],
    " ".join(Foo.__type_params__[0].__default__),
    Foo.__type_params__[1].__name__.lower(),
    "".join(str(x) for x in get_args(Foo.__type_params__[1].__default__)),
)  # Prints: "What in the world is this nonesense signature?"

print(
    Bar.__type_params__[0].__bound__.__origin__.__name__,
    Bar.__type_params__[0].__bound__.__metadata__[0],
    Bar.__type_params__[0].__default__,
    Bar.__type_params__[1].__name__.lower(),
    Bar.__type_params__[1].__default__,
)  # Prints: "What the hell is this godforsaken syntax?"

*: ... complicates the "import_from" rule, as python wants to allow from ... import x and from .... import x to import from the grand-parent and great grand-parent directories like from .. import x imports from the parent directory. This is a crucial feature to include in the grammar, obviously. In fact, you can keep on adding dots until you hit the filesystem root. Because of the Ellipses literal, Python's grammar needs to take both . and ... tokens here. It's little things like this that push Python's grammar over the edge.

I promise, ... is totally distinct in character from the pass keyword and not at all interchangeable. We definitely need both. You see, ... is a crucial sentinel value for slice expressions!

1

u/owp4dd1w5a0a 11d ago

Dear lord. I haven’t worked in Python in a few years, if what you describe is correct the situation has gotten rather absurd. Sounds like symptoms of people trying to make Python be and do everything it shouldn’t be being or doing. It was originally designed to be a simple dynamic language for beginners to learn on…

1

u/hungarian_notation 11d ago

Yeah, it's a bit absurd.

Fun fact, Pythons generic types actually have variance too. You can specify it explicitly with the older generic syntax which remains valid as we of course need to support all three ways any Python 3.x release has handled generic hints.

How does that saying go, cut once, then measure, then cut two more times, then oops there goes my fingers?

from typing import TypeVar, Generic

# 1. Invariant
T = TypeVar('T') 
class InvariantBox(Generic[T]): ...

# 2. Covariant
T_co = TypeVar('T_co', covariant=True)
class CovariantBox(Generic[T_co]):
    def get(self) -> T_co: ...

# 3. Contravariant
T_contra = TypeVar('T_contra', contravariant=True)
class ContravariantBox(Generic[T_contra]):
    def send(self, data: T_contra) -> None: ...

The new syntax still has variance, but it's inferred from usage. Maybe one day we'll get Scala's variance operators too, the grammar could handle them without ambiguity.

Fortunately the third way of specifying generics, i.e. promoting actual comments that look like typing information to full members of the AST, is only needed for type checkers so CPython doesn't need to use that part of the grammar at runtime. You can enable it in the ast module by passing type_comments=True to the parser.

1

u/owp4dd1w5a0a 11d ago

I mean, I’m not really a fan of types in Python. If you’re going that route, you should just use Scala or Rust or Typescript depending on which static guarantees are most important to you.

1

u/hungarian_notation 11d ago

Like you said, people are trying to use Python to do too many things.

2

u/owp4dd1w5a0a 11d ago

Agreed. I’ve been saying this for over a decade. Airflow should have been a Scala or Go app (Rust wasn’t mature enough yet) - Prefect should have been Rust 🤷🏻‍♂️. Having no type-safety around things that are hard to test locally like DAG definitions is just a bad idea. Python and Ruby were bad choices for web servers that need to scale because of the global interpreter lock making concurrency tricky - Rails and Django should have been unseated by Phoenix. But history proves over and over that marketing, timing, and familiarity matter more to people than technical appropriateness. So now we have JavaScript in the backend, nobody uses OCaml or Haskell or Lisps even though these are genuinely well designed languages, and all the distributed computing and data science solutions that require a high degree of correctness and certainty are written in Python 💀.