Today, while working on a project of mine, I noticed this snippet in typeshed:
# stdlib/subprocess.pyi
class CompletedProcess(Generic[_T]):
# morally: _CMD
args: Any
returncode: int
# These can both be None, but requiring checks for None would be tedious
# and writing all the overloads would be horrific.
stdout: _T
stderr: _T
This seems like almost a perfect example of what I said earlier:
This also means even if you run typecheckers, you’ll only catch this when a runtime error happens.
With PEP 823/824, this kind of thing gets a lot less tedious and more correct:
error = result.stderr?.strip() ?? "command failed for unknown reasons"
And maybe in post PEP 823/4 world, typeshed may be updated to reflect reality (im not too aware of their backwards compatibility policy)
To be clear: I’m not saying typeshed did anything wrong, they made the pragmatic choice and one that’s likely less frustrating to users.
I don’t think this is a good example of the ?. operator. In theory, subprocess.run should have some overloads that return one of four different types, e.g. CompletedProcessNotCaptured, CompletedProcessWithStdout, CompletedProcessWithStderr, and CompletedProcessWithStdoutStderr. But for simplicity, they didn’t make this too complicated. When I explicitly pass capture_output=True, I know the result can’t be None, so I don’t want to use ?. to add noise.
For me a syntactic sugar that allows us writing: target_population = continent?.largest_country?.capital?.population ?? 1.000.000
rather than: target_population = (continent.largest_country.capital.population if continent and continent.largest_country and continent.largest_country.capital and continent.largest_country.capital.population else 1.000.000)
This is precisely why people like me are hesitant about this syntax. I have coworkers who would create data structures that work like this if it was this easy. But the code is much easier to grasp if these fields aren’t all nullable.[1]
Yes I know the ideal approach would be for people to be disciplined. But when management doesn’t see the value in discipline, and some coworkers don’t see the value in readable code, I do prefer working with a language (like python) where some degree of discipline is enforced by the language.
I can see the appeal because in practice it is often the case that one of those 3 steps will have to be nullable for one reason or another. But still, 1 is much better than 3. ↩︎
Big +1 from me, it will make almost any API-related work simpler to read and write, and, in many cases (because of the more explicit None-checking Vs existing shorter lazier falsy-checking, or the given examples of just assuming non-nullness generally) more correct.
Geographically true. Also Antarctic doesn’t have inhabitants (let alone 1,000,000), and Swiss cities are significantly smaller than the default 1,000,000.
Meaning, special cases (in geography) need special handling (in coding). Lumping together various missing attributes simplifies data beyond credibility.
That’s a good point. I’m conflating a few things here. My function doesn’t support the initial check for the object being None. getattr does not support nested attribute access and operator.attrgetter supports nested access but lacks a default return for an attribute not being present.
Having used the JS/TS equivalent and Rust equivalent, I find that sometimes it feels like sprinkling magic pixie dust over the code.
In JS case, it’s an acceptable cost… albeit there are some null/undefined/falsy quirks and IDE/language server/ESlint becomes almost essential for producing quality code: some automated tool is needed to remove redundant question marks, especially so when refactoring or changing library versions: some place else ceases to be nullable and now code here needs a fix.
In the cast of Rust, the mental overhead was just too much for me. Because of the type system, inference and other concise syntax, it just gets garbled in my head.
I wonder where on this spectrum Python may land.
For example, where would # ruff: ignore[...] go when the code works around imprecise types? That is the type system says you don’t need a question mark here, but at runtime, you actually do.
I gave this more thought, and while I’m still generally positive on 824, I’m negative on 823.
The issues for me here is that I still think that this encourages APIs that are worse than they should be.
In my experience, it’s both rarely the case that I’ll have nested nullable data, and it’s practically always incorrect to short circuit when I do have it. In the very few cases I have it, I need to be able to handle the different cases for why, so short circuiting and getting None doesn’t accomplish that. That is to say, a?.b?.c is a mistake, because a being None is behaviorally significantly different to b being None. If these weren’t significantly different, then this shouldn’t be nested to begin with, and the data is modeled poorly.
So it seems to me that this further encourages API designs that I don’t think we should be encouraging, on the basis of “people get external data they don’t control the shape of”. Even if that’s the case, handling that yourself in one place or using a library that can structure it to a more reasonable shape for you (eg. cattrs), means that you do that once, and then the rest of your program no longer has to deal with that.
I feel you may be overreacting, or perhaps your use of Python is significantly more structured than many other Python users, or perhaps the types of data structures you have to deal with is different than the data structures that others encounter.
I’d also say that I expect a?.b?.c to be quite rare. We shouldn’t exclude it from the grammar, because it has some applications and its semantics are clear once you know about single ?., but I’d say that it’s pretty rare to encounter doubly-nested optionality.
To quantify “pretty rare”, I asked an AI to count various occurrences in one TypeScript project I co-own(ed), and it summarized the results as follows:
Across 1,238 tracked .ts files:
Standard property chains such as a.b or a.b.c.d: 61,796 uses.
Property chains containing optional chaining, such as a?.b: 1,602 uses. This is 2.59% as common as standard property chains, or roughly one optional chain for every 38.6 standard chains.
Optional property chains with two or more ?. occurrences in the same uninterrupted chain, such as a?.b?.c: 69 uses. This is 4.31% of optional chains and 0.11% of standard chains.
Comments and quoted string/template contents were excluded. Each maximal chain made exclusively of identifiers separated by . or ?. was counted once, so a.b.c.d counts as one use.
I have to admit that I don’t quite follow your reason for expecting this to encourage inferior API design. Can you show an example of how using cattrs helps structure the data to avoid None? Suppose a user may or may not have a subscripton. If user.subscription is None, what would you want change it to? What’s bad about having an optional subscription field (not possibly missing, but having a type of the form S | None). Or perhaps you can work through some other example where you consider nullability a bad design? (Or if you already gave such an example and I missed it, just link to it?)
If you only need the single, then isn’t ?? (824) reasonably sufficient?
It’s specifically the nested nullability where it becomes an issue. Given an earlier example of:
The answer here is that the short circuiting None handling is still not ideal here. In what way is a default of 1.00.000 useful? In what way is any default value useful?
If a user requested the population of the capital of the largest country on the continent, and the continent in question is antarctica, you should display an error that antarctica doesn’t have a largest country. That requires knowing that largest_country in that API is None.
(Now why that’s an attribute or property is another question)
None is certainly both useful and special, but it’s also something that should be explicitly handled where it is necessary to allow, because the reasons for it’s existence when looking at nested access aren’t typically the same for each level of access.
You mean you can write a?.b as a ?? a.b ? That still repeats a, which feels verbose, and if the thing before ?.is a bit longer, the repetition gets annoying:
account_name = user.account?.name
# vs.
account_name = user.account ?? user.account.name
Not to mention something like
account_name = get_account(user)?.name
# vs.
account_name = get_account(user) ?? get_account(user).name
Of course we should write the latter using a temp variable. But I would really much rather have an operation using the stack than having to explicitly create a temp variable – especially since, as it’s already a nuisance to invent, I’m not going to waste more code to GC that temp variable.
I don’t otherwise have a strong opinion on the topic, though I would like to propose to make ?. and ?[] not raise AttributeError / LookupError if the attribute or item is not present, rather they should return None.
The use of ? has made clear that the author intends to use None rather than exceptions as the absence mechanism. In situations such as incoming JSON where missing values aren’t marked by null (and thus the deserializer doesn’t produce None) but by omitting the key altogether, ?[] as proposed originally doesn’t help much as it still raises KeyError on missing keys.