The soul of the beast: Everything about Python’s grammar - Pablo Galindo Salgado - PyLondinium19
Watch on YouTubeVideo summary
The core subject of this talk is an exploration of what truly defines Python, distinguishing between its interpreter implementation and its underlying grammar. While people often associate Python with the default interpreter, the language's identity lies in its grammar—the specific rules that dictate how code is written and parsed. The speaker illustrates that valid Python code can sometimes be surprisingly convoluted or "twisted," yet still parse correctly because the grammar allows for such structures. To understand these rules, one must look at the formal grammar document, which uses a context-free grammar system with terminals (actual words like keywords) and non-terminals (rules to be expanded). This system relies heavily on the concept of looking only one token ahead to decide which rule applies, a limitation that shapes almost every aspect of Python's syntax.
A significant portion of the discussion focuses on how Python's parser transforms this grammar into an efficient machine using finite automata. The process begins with a non-deterministic finite automaton (NFA), which can have ambiguous paths where the parser doesn't know which direction to take based on a single token. To solve this, the system converts the NFA into a deterministic finite automaton (DFA) and then minimizes it into a simple lookup table structure. This transformation ensures that parsing is incredibly fast by acting like a hash table, but it also forces the grammar to avoid ambiguity. Consequently, Python's grammar is designed with specific constraints, such as forbidding empty productions and ensuring that rules do not start with overlapping tokens, to keep the parser simple and fast without needing complex backtracking mechanisms.
These technical constraints lead to several well-known quirks in Python syntax that users often find confusing or counterintuitive. For instance, the ambiguity between a function call argument name and a list comprehension variable forces the language to disallow certain syntactic patterns, requiring workarounds like wrapping multi-line `with` statements in parentheses even when they are not strictly necessary for grouping. Similarly, the "walrus operator" (`:=`) shares naming conflicts with module names, creating parsing ambiguities that are resolved only later in the compilation pipeline by rejecting invalid code as syntax errors rather than parse errors. These examples demonstrate how low-level implementation details regarding the LL(1) parser limitations permeate up to the user level, dictating what is considered "Pythonic" and limiting the language's expressiveness in ways that seem arbitrary to the average programmer.
In conclusion, the talk reveals that while extending Python with new operators or grammar rules is technically straightforward for core developers—requiring only a few lines of code to define a new token and implement an operator—the impact on the language's ecosystem is profound. Every new rule added must navigate these deep technical restrictions to ensure the parser remains efficient and unambiguous, meaning that even seemingly simple user-facing features are built upon a foundation of complex engineering compromises. The speaker emphasizes that understanding these "tiny technical details" provides insight into why Python behaves the way it does, showing that the soul of the beast is found in the intricate balance between human readability and machine efficiency enforced by its rigid grammatical structure.
Read the full video transcript
so
pablor he again another member of our
team
I remember Pablo joining is a pre FST
many years ago now this is very scary
so with great pleasure his Pavle talking
about the soul of the beast awesome so
thank you very much for coming
so in this talk I would like to go deep
into interesting question which is what
makes Python Python right so we have two
main parts that people recognize usually
as Python one is the interpreter which
is normally when you type Python it
probably see Python the one who will
appears which is the default
implementation but the interpreter can
be done in many languages as well
we have other implementations for
example in Java like JSON or item Python
so that part can actually so see passing
is not technically what would makes
Python Python so there's another part
what people usually recognize as Python
which is the grammar right the the kind
of problems you write and expression as
you do so in this particular talk we are
going to Center on all the world
surrounding that particular aspect of
the language and how tiny technical
details of how is made up will impact a
lot the code you write so I will start
with an interesting question so the
question is is this bollocks Python code
will be like 10 seconds
who thinks is Pollock Python code like
crazy huh who thinks is in Bali
well this doesn't add up right ok so
actually it is Bali Python code and this
is the ast of this thing so you don't
need to understand it just that this
means that python is able to parse this
code so some some clever stuff like this
is actually like - some bigger man and
this is the lips which thing so but it
was simply something horrible right
and yeah that what with this what I want
to say that the code that people
normally recognize as Python is actually
a tiny subset of what the language
actually allows and there is some very
twisted ways of writing valid Python
programs right and in this talk we are
going to try to understand how these
rules are actually
layout and with impact they have okay so
so let us start with some grammar basics
right because we are talking about the
grammar so the grammar is basically a
document that explains what is ballot
pie right so let's start with how this
document is described so we are going to
have rules right and the rules are
basically described as a name for the
rule with social production and then
some description of what the rule is so
you have to be in mind that with the
grammar describes exactly how to
generate Python programs so the parser
actually what is going to do is that is
going to grab the grammar and it's going
to sort of reverse engineer so in the
opposite question which is given a
particular text is dissipation program
that can be generative to the grammar
but you have to remind that grammar is
laid out in a way it actually makes it
very easy to say okay give me a random
Python problem so in that way we will
have rules here and then rule
descriptions on the second side so we
can have many descriptions for the same
rules so this basically means that this
rule can actually produce these two
things instead usually instead of doing
in this way we use the or operator so
this means that this rule can be this
chunk or this other chunk we will also
have the plus sign so the black sign
means one or more this means that this
rule is one letter a or more than one
it's very similar to regular expressions
but it's actually much more powerful
than them but you can familiarize more
or less how to describe this thing if
you have in mind regular expressions so
in the same way we have the asterisk
which means zero or more then we will
have these square brackets which means
this is optional so we will have
something like this rule means that you
can have an a or this particular other
rule and this particular other root can
be optional in that particular example
and then so this is the actual so now
that we have more or less but in mind
this is the actogram a chunk of the ecto
grammar of Python right so let's see how
we interpret this thing so we're
starting the first rule which is file
input and then we say that basically a
file what you can write in a file is
start with a new line and then like zero
or more statements and then the end of
the file
so you say okay what is a statement so
then you go here and you say okay I
statement is a simple statement or a
component statement and then you say
okay what is a simple statement I said
okay it's a small statement or cetera
editor
and then in the component streaming we
see things that we more or less
recognize already right like if
statement while statement so all of
these are things that are possible and
it is that the whole document which this
is only a subset will basically describe
what is possible in their Armour and
then we have to distinguish two possible
inputs here so one is let's go here yeah
so one of these is what we call a
terminal so a terminal here is a word
which is surrounded by quotes this mean
like little it is worse so for example
for the wireless timing we expect the
world while and then we have this non
terminal so if you mind this grammar as
a tree the terminals are the leaves and
a non terminal any intermediate node
right so a non terminal basically
something that you need to expand it
basically refers to another rule so one
thing which is not super important here
but one thing is that if if the
basically the grammar is written away
that is sort of substitution so
basically you have here a rule and then
you have always the description of the
rule at the end it's called a
context-free grammar this other
definition for a but the idea that that
simplifies a lot of things and maybe is
that we have to be we'll see later how
this thing play but it's very important
to this thing with these two terms
because the words that you can write at
the beginning of the rule or the words
that can start the rule will impact a
lot how how certain structures are
allowed in cpython
so I skip this this slide so this is
another part of the grammar so we will
see the statements that we mention
before right so we saw that component
statement can be any of these and then
we start seeing so for example an if
statement is the word if followed by
something that evaluates to a condition
or an object you may that can be true or
false then the column and then this
suite is basically a block what people
recognize usually as a block it's called
here in the grammar suite and then you
have the leaf and all the stuff so it is
that we have this document right with
this kind of rules which basically
describe what is what is possible to
write and what is not so let's analyze
the impact that it has or the particular
things that cpython imposes over this so
particularly the simple grammar is
another
one grammar so let's see what is this so
another one grammar it's a grammar that
is part left to right do the leftmost
derivation which means that you analyze
first the talking which is on the on the
left and the you star spanning from that
and this is the most important rule
which is that when you are parsing like
the parser witness analyzing a
particular program is comparing with the
Rama can only look one token on the on
the like one Tok is not a hat which
means that if I need to distinguish a
rule like mind you have a particular
rule which has two possibilities like
two productions and then I need to
distinguish which one of the two is the
correct one so for doing that I can only
look the next token in the input right
so for example if you are doing
something like for X in range something
and I find the word for if I need to
distinguish two different productions I
can only look one token ahead with will
be these X in example eyeball so it
turns out that this is a very well-known
problem how to solve like this actually
leads to very simple parsers but the
Python grammar has two more
particularities that may the parts are
even simpler for the ones that know more
a bit of grammar theory basically this
is a partially generated with a partial
table with what is known as the first
cells and the follow sets more on this
later but particularly we have two more
things here one is that the Python
grammar doesn't allow empty productions
this means that every rule has to be
something that doesn't and in the empty
string so an empty production is
basically a rule that can be empty and
this makes the parts are more difficult
because if you have rules that can be
empty and you can only look one token
ahead then if the rule is empty you need
to actually look to so that's what leads
to what is called the follow sets so we
because we don't need them I know in
displaying this tone and then we have
another kind of rule which is a bit more
lucy and this is not actually enforce
but is basically the grammar is laid out
in a way which means that in some levels
only the last alternative route and have
a non terminal so for example we can she
oops okay so for example let's see this
one so if you see this so the flower
stamen so we have break continued return
price and dill so you check all of them
except jill they start with a
non-terminal right so for example break
a thermistor with break continuous ten
minutes that we continue let's address
Adama the only one who actually start
with an
terminal is deal statement which is here
a non-terminal called deal expression so
this is more or less layout and this
simplifies a bit more how the first sets
are layout on and it makes easier to
produce the parcel so okay so this is
more or less the grammar so let's let's
describe what is these first sets so the
first sets for a rule for example a
simple statement are all the non
terminals the rule can start with and
this is very important because as we saw
before if we are only allowed to look at
the next token and we we need to see if
a particular rule is valid it will be
very interesting to know all the
possible tokens that rule kind of start
with right because if we have that token
and the token is in one of these
particular non terminals that are
allowed then we know that we are good
because it's possible that that rule has
the start with that token but if we have
a particular token and then we are
trying to pass a rule and that rule
doesn't start with that token we know
that is invalid so we can say that it's
a syntax error right so this is what is
called the first set and it's going to
be very important later so this is the
first set of sound of the expression so
for example playing can only start with
Prine grayskin alistair we raised but
simple statement can start with all of
these things okay so let's see how the
parser works so see Python actually
doesn't have a hand written parser it
has a parser generator so it's a tool
that generates the parser for us from
the grammar file which makes it much
much more less error-prone because you
don't need to manually modify the parser
every time you generate more grammar
rules so let's see how it works so it's
actually a very simple thing so we start
with the grammar in extended backwards
not form which is the document that we
saw before and this will product this
will produce non-deterministic finite
automata so we'll see what it is
basically these things are just control
graph flow so you like this kind of
drawing when you have nodes and arrows
telling you which option you have we'll
see some examples and the things that
this these initial things are very
simple to produce but they are very
complicated is full of errors and
complexity so the the step that grabs
one of these and convert them in with
this correlator manectric final automata
is basically simplifying it and making
it more more easy to to follow so let's
see one example so let's say we start
with this rule which is the rule for
fact
right like a plus B a minus B not
something right so this is this is the
rule so then the first thing that the
part the partial generator produces the
non-deterministic finite automata which
is this controller right so we will
study in this state and then we will go
down until we start seeing these these
tokens so the idea is that you start
here and then you read some particular
program that is trying to follow that
rule and if you are able to go from the
state to the end then the rule is
correct if you are not able to go to
from the start to the end then the rule
is invalid so you mind that instead of
plus minus and till add you have
asterisk right which is not part of this
rule in particular so this didn't you
kind of follow here because there is no
step that has the stage it's actually
hidden in power so it's a valid Python
program but if you don't look at power
for now you can imagine how you cannot
follow this in now so this is very
complicated because in 30 you can only
follow the arrows if one of the tokens
is allowed so it for example for the
stay for two state eight is very easy to
know if you can follow because if you
find a minus sign then you can go ahead
but if you don't find a minus sign that
you cannot follow it and the problem is
that we have here in the
non-deterministic automata we have some
stages which we don't know if we can
like follow them or not for example for
going for in state zero to state one or
two we don't have any particular sign to
follow so that is a problem because we
don't know if we can wear the rule to
transition to the other states or not
that's the complicated part of the
non-deterministic finite automata so the
next step is to produce a deterministic
one which is that one so in this case as
you can see all the states the all the
transition between a state and something
is followed by a particular token so for
example if one in a state 0 we just need
to look on the next like token in the
input either plus minus or till and if
it's not any of these we say that the
rule is invalid if it's one of these
then we follow it and we Furr go to
state one and state one goes to another
rule call factor but as you can see in
this particular brew a DFA sorry yeah
DFA there is not transition between
state and States with our unknown
transition but you can see something
which is still bad which is for example
this node which is here isolated this
node is rubbish it doesn't mean anything
it just means that when we transform
this thing into this thing we produce
some things which are either tremely
complicated or they can't they
basically Travis which has no States so
the next step is simplifying the thing
into what is called the final defect so
basically is removing complexity and
removing this this particular node so
this is basically what it is is
producing this kind of control flow
diagrams which are still very
complicated with some particular
algorithms you transform this to this
which is very known and all and from
this you run some simplification which
is called minimization algorithm and
then you produce those here and this
this particular diagram is what the
parser follows if you think about this
is more or less like a hash table or a
dictionary so basically during a
particular state and then you look at
the possible keys here are plus minus or
tilde and if you have them in a
dictionary or the hostel then you
transition to whatever value you have
and if you don't you say that the rule
is invalid so that makes actually the
parser so much so fast because it's
basically some sort of hash table look
up again and again and again next and
these these rules here so for example
when you owe to power like the square
box in power you have another of these
basically describing with a particular
sub release so it's basically nested
dioramas instead of nested diagrams so
the sixth time examples here so for
example we can see the roof for
comparison so in this example you always
will have the the complicated one right
a non DFA on the left then the DFA here
and the simplified one here so you more
or less can can see what kind even if
you notice I made the algorithm which is
not super complicated I will take a lot
of time to explain you can see more or
less or you can feel the kind of
simplification that the parser start
doing so this is for example for
comparing things between them this is
the rule for like the comparison but
with the actual tokens so you will see
things that you recognize as comparison
operator like note equals less than
bigger than so you can see actually how
when the rule is actually a bit more
complicated the non DFA is actually a
monster and this non DFA is actually
very well simplify I didn't put the one
in the middle because it still it
doesn't fit in in the particular screen
so we can see more so for example this
is the decorator and this is a very very
interesting example because you can see
actually from a very high point of view
what you can decorate so for example in
the room for decorative things you can
see that
integrating functions class the finishes
and functions and you cannot decorate
anything more because the rule doesn't
allow that and again we will see a way
of adding things to this so you can play
a bit later but you can see how these
things are actually even if you don't
see the rules and only you see this kind
of automata switch on something lower
level you can still recognize more or
less what is valid or what is describing
more of these you have the factor
internal we saw before so you can see
how here in the non DFA we have still
this transition between states and in
the like and one we have only transition
between the two become numbers and you
can see how they they actually much more
simplify so I want to show you my
favorite one which is this one so this
one is the the diagram that parses
function definitions which is this
monster but don't worry because this is
the non DFA right so let's see the
simplified version which is much clearer
to read this not so this is the most
complicated rule but this is a reason is
so complicated and this is one of the
things we are going to see at the end so
the reason it's so complicated is
because the way we need to describe this
rule is actually fighting the ll one
limitation so the little one a lot of
people see it as a blessed because they
say okay because it's a so simple
grammar that has this very strong
conditions that make them like very
strict then it's very difficult to have
like very complicated rules which is
good because at the end you want Python
to be simple and readable and like not
like other languages C++ so but there is
a problem which is that sometimes what
the user perceives has a very simple
rule is actually not that simple or at
least you cannot describe it there's
another one rule and in that particular
case you need to do a hack and you will
see many hacks in the grammar which is
called full leaf expansion which is
literally grabbing all the tree and
expand it at the end because you cannot
have like these limitations will arise
in a way but there is there is a way of
solving this limitation which is having
a rule full of non terminals and in that
way you don't have this problem because
you don't need to deal with first
because it's all non terminals and the
reason this particular rule is so
complicated is because this is one of
them
this is the most complex example of the
most beast example okay so let's
continue so let's see what limitations
on the 11 grammar are so one particular
example for example they see let's see
these rules so this rule we have is
whatever rule and it will start with the
word do then some particular sub rule
called a building while an expression
and then we have another possibility for
the rule which is the same style except
that it has another rule call B and then
we go to the first set we remember are
the non terminals at root canister with
and we see that a can start with the
letter A or the letter B and B can start
with a little a or the letter C right
then let's say we are trying to parse
this input right there you should write
that and we want to see if is a correct
program in this case we know that is the
first choice like the first rule because
if we go to the first set this B can
only be on the first set of capital A so
we know is this particular here right
because this a can only can start with a
alket sorry with B and we have B so the
option B doesn't have been inside so we
know it's not B but let's say the user
writes that code so now we have a
problem because this particular
lowercase a appears in both so is this
one or is this one so we don't know and
this makes the rule ambiguous because
you don't know which one of the to write
because if you go with all the tokens
that the rule can start they share one
of these non terminals so when you find
this a you don't know which route to
choice and this is a problem because
this route is embodied in another one
and this is the limitation right this is
what makes ambiguous do you cool if you
have a more powerful parser and then you
say ok but in mind that I'm able to like
follow and read more tokens from do
backtracking
then you can distinguish this one or
this one but because we are only allowed
to look at the next token we we cannot
write so we're passing do and then we
look at the next token we find a and
then we know we don't know what to
follow and that's the problem so this
happens in C Python so actually it
happens a lot so for example this is the
rule that describes how to pass
arguments how to code functions and
particularly I want you to look at this
argument rule so let's let's see what
the argument is okay so we have test is
basically an object so 3 4 X
whatever then we have the keyword woman
so a particular name equal and an object
and then we have a star star like
dictionary unpacking and listen package
right so this one is actually not not
valid because if we look at the first
sets of tests so this one it actually
contains name so these two are ambiguous
because if you are here and you say okay
I will I want to choose this one or this
one and then you find name you don't
know if you choose this one because test
also start with name so you don't know
which one to follow
so this makes these rules not possible
but you can still call functions in
Python right so how is made so this is
one of the first hack we have which is
the actual rule is test equal test and
this rule allows to through things like
list comprehension equal to this
comprehension or dictionary
comprehension equal to the canary
comprehension and then you say wait wait
but I cannot write a same Python right
so the reason is that we allowed of
thing on the parser well the parser is
pretty happy with that
but when it comes to the ast which is
much later in the in the compiled
pipeline we say by a way this test
actually anything other than just a name
like keyword if it's not then say syntax
error it's just a syntax error that can
happen on the parser but you don't know
so little lies that we have another
interesting one is the do you know the
water separator so the water separator
also suffer this so this is the rule for
the water separator which is like a
particular name The Walrus operator and
an object and this has the same problem
right because the first set of tests
also include name and you say hey but I
don't see the pipe right so it's not
this or or this thing but you think
about is the same case because because
this is optional right so if you are
saying okay I need to pass this thing or
what I actually have this optional path
so because you can only look one token
and both can start with a word name like
a non-terminal name which is any Python
module then you know what to choose so
again you have the same problem and the
actual rule again is this horrible thing
and then we force it do it much later as
you can imagine this is not very like
maintainable and then we have one that
Lukas here knows very well so this is
with statements right and this is valid
Python 3 so in Python 3 you can have
multiple
context managers in the same with
expression so you for handle you can say
with a sxb sy and then if you have a lot
of these then you say well I don't want
to put all of them in the same line so
if you want to continue you use you need
to use this line break character
right which is sort of okay except that
this theme really mess with indentation
and out of formatter tools like black
cannot really format this thing very
what so you could do this thing if you
allow this thing which is put
parentheses around right so you see
something like okay so with open
parentheses and then you put all your
context managers and then you close
parentheses this this particular
constructed but is it 4 out of 4 matters
because they know what is the limited
here right except that this is invalid
and you say why it sounds like I mean I
have seen this thing before right I seen
the scene in the streams I say imports
right you can say import an open
parenthesis and a bunch of things well
it turns out that if you analyze the
rule which is this one and then you want
to allow the parentheses then you say ok
so I'm going to write the same rule so
with and then I'm going to put two
possibilities right the old rule and
then I'm going to say open parenthesis
the old rule and closed parenthesis
right so it's either the rule or the
rule in between parentheses so again the
point of this is that this is ambiguous
because this guy which is the item also
kind of start with the open parenthesis
so you say with open parenthesis now you
don't know if the parentage belongs to
because uy3 between parenthesis or
because you are riprap grouping with
parentheses the whole rule right and
this is sad so we cannot improve in that
I'd really try okay so you will see
right so we have this dual thing in how
we square the grammar we have a very
powerful and simple way to describe
because we have another one restriction
and this makes not only the parts are
very fast but also very simple to
describe but then we have all these
weird things because we we want to put
some rules that we know they are simple
enough for users to use but all these
technical restrictions rest it what you
can do and I want you to post here and
make you think about this because the
fact that they ruled the grammar is
little one and all these things is
they're actually very technical
implementation details right like this
is something that the user doesn't need
to know but this thing which is of the
core like three
deep down this thing right now is
percolating outside right like even if
you are not a like writing cpython and
you're writing your own interpreter and
you want to fulfill Python grammar even
if you have the most powerful parser
right now you want to run Bali Python
you cannot write this which is weird
right it's like the implementation
detail are portioning something as
important as the grammar and and this is
like sort of dangerous and like worrying
and beautiful at the same time how this
tiny technical decisions can can affect
so much like what is a white Impala
Python program so at the end just to
conclude this particular step so with
the person produces what is called a
parse tree so if you write x plus y
following all these DFS and basically
the arguing that we saw it produces this
particular structure and if you
substitute those numbers to the actual
rules you will have this right so that 3
plus 4 or 1 plus 14 or something is this
particular work which is basically a
flattened out version of the diagram
that we saw before so this is what is
fed to AST and we are going to stop here
because we are going to talk asti's so
this is when the parser ends so what I'm
going to show you right now as a
application is how imagine that you want
to create a new grammar rule in C Python
or in Python right like I won't extend
Python for playing array so or or how we
core developers actually write new Rama
rules so they say you want to write this
rule right so you have two objects and
then you want to implement the arrow
operator which is something that the
user can implement right so the user in
my in that the user has a class and the
class implements underscore underscore
arrow on the score underscore and you
and that's about something right and you
want to allowed to write a Aerobie and
it calls this particular function right
in the same way when you write a plus B
it goes down there ad so first you need
to do is up this token to the tokenizer
so sorry to the grammar so you go to the
grammar then you find the rule
describing multiplication division and
all the stuff and then you add the arrow
here right so we have the star the slash
all the things that are allowed and then
we say also the arrow so when you add
this thing and then you write the parser
generator you need to include the token
as a valid token right so in you go to
this particular file which is in C so we
are going to
see a lot of sicko and then you say okay
there is a new token which is going to
be called arrow and we are not explained
with token ease at this point so we just
define the existence of the token so the
idea is if you are ok Shangri two of us
know already very fast okay so then you
go to the tokenizer and describe how to
parse the scene so you find like a minus
sign and then follow like the bigger
sign and you say okay that's the arrow
and you say return the arrow then if you
run the token I said you will see that
the token I she already is able to parse
the arrow so it's actually very simple
to do even if you don't understand how
I'm doing these things so these three
lines here is the only thing you need to
do for the ast two parts so you don't
need to understand it you just need to
say that if you know what you're doing
is actually very simple and then finally
do the only thing you need to do is
implement the actual operator so in this
case this is the bicycle for that which
means like you find a R or B then pop a
poor beat call the function we described
in the arrow and then continue with
interpreter which is again very simple
you know what you're doing and finally
you just need to describe that there is
something called arrow and right arrow
which can be describing the objects
which is again these three lines plus
the header file and with that you can
try this code so you can create a class
that implements understand the score
arrow and in this case I'm going to map
the operator to the other and then you
can write this code F like our row B and
it basically Maps the function over the
list so as you see all these particular
ending trainer so if you see all these
particular like things make implementing
new rules of the Wonder which are right
now extremely simple but they carry out
all this danger and all this impact on
on the language that you see and
appreciate so thank you very much I'm
sorry for sooo possible
you