As soon as you use the yield in the function body, it becomes a generator. A call to the generator function returns this generator object. This is no longer a normal function; instead, the generator object took control.
From yield expression documentation :
Using the yield expression in a function definition is enough to force that definition to create a generator function instead of a normal function.
When the generator function is called, it returns an iterator, known as a generator. This generator then controls how the generator functions. Execution begins when one of the generator methods is called.
In a regular function, calling this function immediately switches control to that function body, and you simply check the result of the function specified by its return . In the generator function, return still signals the end of the generator function, but this causes a StopIteration exception StopIteration . But until you call one of the four generator methods ( .__next__() , .send() , .throw() or .close() ), the body of the generator function does not execute at all.
For your specific function f() , you have a regular function containing a generator. The function itself has nothing special, except that it ends earlier when return 3 is executed. The expression of the generator on the next line is in itself, it does not affect the function in which it is defined. You can define it without function:
>>> (i for i in range(10)) <generator object <genexpr> at 0x101472730>
Using a generator expression creates a generator object, similar to using the yield function in a function, then calling this function creates a generator object. So you could call g() in f() with the same result as using the generator expression:
def f(): return 3 return g()
g() is still a generator function, but using it in f() does not make f() a generator function. Only yield can do this.