Rather than embed a generator expression in a single call to dict, build your dict as you process in the input.
n = 1000
students = {}
for _ in range(n):
x = input("enter students name and grade. Example 'Student C: '")
if x == "quit":
break
# You might want to do more error checking here.
student, grade = x.split()
students[student] = grade
A little more advanced, you can make use of the two-argument form of iter (see below) to terminate the loop if quit is entered, and use itertools.islice to limit the loop to n entries.
from itertools import islice
n = 1000
students = {}
for x in islice(iter(lambda: input("..."), "quit"), n):
student, grade = x.split()
students[student] = grade
As you might notice, this allows you (ignoring any error checking while splitting the line of input) to go back to your original idea of a generator in a call to dict:
from itertools import islice
n = 1000
students = dict(x.split() for x in islice(iter(lambda: input("..."), "quit"), n))
Finally, if you were only using n as a way to avoid reading input forever, you can omit it, as the user can now enter quit any time they like:
students = dict(x.split() for x in iter(lambda: input("..."), "quit"))
Also, another way to create a zero-argument callable to use as the first argument to iter is to use functools.partial:
from functools import partial
students = dict(x.split() for x in iter(partial(input, "..."), "quit"))
What does iter(x, y) do?
(The two-argument form of iter takes a function to call repeatedly, and produces the return value of that function until the function returns the second argument. iter(lambda: input("..."), n) is equivalent to
def iter(f, stop):
while True:
x = f()
if x == stop:
break
yield x
)
What does islice(x, y) do?
This produces a new iterator that returns only the first y elements of x, if it has more than y elements. It is roughly equivalent to
def islice(x, y):
while y:
yield next(x)
y -= 1