120

I am using Python 3.6.1, and I have come across something very strange. I had a simple dictionary assignment typo that took me a long time to find.

context = {}
context["a"]: 2
print(context)

Output

{}

What is the code context["a"]: 2 doing? It doesn't raise a SyntaxError when it should IMO. At first I thought it was creating a slice. However, typing repr(context["a"]: 2) raises a SyntaxError. I also typed context["a"]: 2 in the console and the console didn't print anything. I thought maybe it returned None, but I'm not so sure.

I've also thought it could be a single line if statement, but that shouldn't be the right syntax either.

Additionally, context["a"] should raise a KeyError.

I am perplexed. What is going on?

martineau
  • 119,623
  • 25
  • 170
  • 301
justengel
  • 6,132
  • 4
  • 26
  • 42
  • 2
    Does this answer your question? [What are variable annotations in Python 3.6?](https://stackoverflow.com/questions/39971929/what-are-variable-annotations-in-python-3-6) – Georgy Aug 08 '20 at 11:28

2 Answers2

117

You have accidentally written a syntactically correct variable annotation. That feature was introduced in Python 3.6 (see PEP 526).

Although a variable annotation is parsed as part of an annotated assignment, the assignment statement is optional:

annotated_assignment_stmt ::=  augtarget ":" expression ["=" expression]

Thus, in context["a"]: 2

  • context["a"] is the annotation target
  • 2 is the annotation itself
  • context["a"] is left uninitialised

The PEP states that "the target of the annotation can be any valid single assignment target, at least syntactically (it is up to the type checker what to do with this)", which means that the key doesn't need to exist to be annotated (hence no KeyError). Here's an example from the original PEP:

d = {}
d['a']: int = 0  # Annotates d['a'] with int.
d['b']: int      # Annotates d['b'] with int.

Normally, the annotation expression should evaluate to a Python type -- after all the main use of annotations is type hinting, but it is not enforced. The annotation can be any valid Python expression, regardless of the type or value of the result.

As you can see, at this time type hints are very permissive and rarely useful, unless you have a static type checker such as mypy.

vaultah
  • 44,105
  • 12
  • 114
  • 143
  • 14
    Shouldn't this require an `=` assignment operator then? The key doesn't exist. This just feels wrong to me. – justengel Jan 18 '18 at 14:29
  • 1
    In this case, `:` *is* the assignment operator. We're just "assigning" a type annotation alone, not a key. I doubt there's any reason for allowing it, just an unintended side affect of adding the annotation syntax. – chepner Jan 18 '18 at 15:04
  • 1
    @chepner It seems this is no side-effect imho. This is exactly what the corresponding PEP was designed to do. – Ma0 Jan 18 '18 at 15:10
  • 6
    It's weird that it'll allow you to annotate a target that hasn't yet been defined though. If my very first line is `x: str` and immediately followed by `type(x)`, the interpreter will raise a `NameError`. IMO the syntax should enforce the object is pre-defined, or is defined on the spot. This just introduces confusion. – r.ook Jan 18 '18 at 15:21
  • 3
    @Idlehands This defeats the purpose though. Having `x = 'i am a string'` prior to `x: str` makes the latter kind of redundant.. This shouldn't have been added at all. It was fine as comment; I never show it used one way or the other. – Ma0 Jan 18 '18 at 15:25
  • 1
    @Ev.Kounis When you are converting a dictionary definition to an assignment `d = {"a": 2}` to `d["a"] = 2` it can cause a very nasty typo that is hard to find. Especially in django when the dictionary is a context and you only use that key in a template. In that case it never raises an error and everything just works in a very wrong way. – justengel Jan 18 '18 at 15:39
  • 1
    @HashSplat Sounds like a strong candidate for testing. This would break your code no more than `d["a"] = {2}` would, which is IMO an equally easy-to-make typo. – Schism Jan 18 '18 at 18:46
  • @Idlehands Having `x: str` wouldn't affect `type`, a class called on _the value of `x`_. – wizzwizz4 Jan 18 '18 at 19:34
  • 3
    @Idlehands Allowing this syntax probably enables you to use type hinting in function definitions. In `def f(x: str): ...`, `x` is also not defined at the moment of being annotated. – Graipher Jan 18 '18 at 22:09
  • Does this mean that, since `2` is an `int`, that element is hinted to be an integer? – RonJohn Jan 19 '18 at 06:30
  • @HashSplat This doesn't do anything (may have little impact on run time in case of NameError etc.). Its purpose is to be a hint for static analysis, nothing more. It's clearly explained in the linked PEP. – BartoszKP Jan 19 '18 at 10:24
  • @justengel I made the same mistake today and took me a while to figure out the exact issue. – Ganesh Satpute Oct 07 '19 at 11:51
1

The annotations are automatically stored in __annotations__ which is a dict. For x: y. y must be a valid expression, i.e. y, or whatever is on the right side of the :, has to evaluate. On the other hand x ,must, at a minimum be able to be a key, thus hashable.

In addition, the LHS cannot be a set, because sets are unhashable, >>> {2}: 8

SyntaxError: illegal target for annotation

nor a list: >>> [2]: 8

[2]: 8 SyntaxError: only single target (not list) can be annotated

nor a tuple:

>>> (2,3): 8 (2,3): 8 SyntaxError: only single target (not tuple) can be annotated

ShpielMeister
  • 1,417
  • 10
  • 23