I’m not sure it this will make it more or less clear, but there is a lot going on here in that one line of code 
For loops:
for x in something:
iterates through “something” (which must be an “iterable”[*] – often a sequence of some sort). each time through the loop, x gets assigned to the next item in the iterable. A very simple example:
In [1]: for x in (4,1,6,3):
...: print(x)
...:
4
1
6
3
dict.items()
:
A dictionary’s items()
method returns an iterable of the “items” in the dictionary – each iteration will return a tuple of one item: (key, value):
In [5]: symbol_count = {
...: "A": 2,
...: "B": 4,
...: "C": 6,
...: "D": 8
...: }
In [6]: for an_item in symbol_count.items():
...: print(an_item)
...:
('A', 2)
('B', 4)
('C', 6)
('D', 8)
Sequence unpacking:
If you assign more than one name, Python will “unpack” the seqence on the right, and assign them to the names on the left:
In [7]: a, b = (5, 6)
In [8]: a
Out[8]: 5
In [9]: b
Out[9]: 6
Note that you don’t need the ()
on the right – the comma will also make a tuple that can be unpacked.
In [10]: a, b = 5, 6
In [11]: a
Out[11]: 5
In [12]: b
Out[12]: 6
Putting this all together, you can also “unpack” in a for statement, so that the (key, value) tuple being returned by dict.items() gets auto assigned to the two names provided:
In [16]: for key, value in symbol_count.items():
...: print(f"{key=},{value=}")
...:
...:
...:
key='A',value=2
key='B',value=4
key='C',value=6
key='D',value=8
I couldn’t help myself but introduce the nifty f-string debug options, too 
so this line:
for key, value in symbol_count.items():
Is actually doing all of this:
items = symbol_count.items()
for item in items:
key = item[0]
value = item[1]
Clear as mud?
[*] There’s more to learn about iterables, but in short: they are something you can put in a for loop that will returns items until done.