I am 'expecting' b to be available after the function call, as I have returned it
The fundamental misunderstanding: you aren't returning a variable, you're returning a value. Variables are labels (names) for values. Inside the function, you have a name b for a value "foo". You use this name to refer to the value, so that you can return it.
On the outside, to use this value, you must do something with it. The call to the function - func_1() - is a stand-in, in whatever expression contains it, for the value that gets returned. So a line of code like func_1(), by itself, does nothing visible: you have an expression that evaluates to "foo", but then nothing is done with "foo".
You could, for example, print(func_1()) to display the string. You can assign the returned value to another variable: c = func_1(). Now c is a name for the "foo" value that was returned. You can use the function call as part of a larger expression: c = func_1() + "bar" - now c is a name for a string "foobar", created by joining together the string returned from the function with the directly-specified one.
On the other side: to get information into the function, you should pass it as a parameter. These are the values that go inside the parentheses: when you write the function, you put argument names (another kind of variable) inside the parentheses, and when you call the function, you put the parameters that you are calling the function with (expressions that provide the values). Now, inside the function, each time it is called, the arguments will refer to the parameter values.
Let us work a simple example. We will make a function that appends "bar" to an input string, and call it a few times.
def add_bar(to):
return to + "bar"
Notice how we can return any expression. When the function is called, the expression is evaluated, joining the passed-in argument string to to the constant "bar", and the resulting value is returned. There is no variable here that represents the entire return value, because again, we are returning a value, not a variable.
Now we may call this. Again, the parameter can be whatever expression - so a constant:
add_bar("foo")
or a variable that we have previously assigned a value:
my_string = "sand"
add_bar(my_string)
or something more complex, maybe even involving another call to the function:
add_bar(add_bar("bar ") + " ")
But again, none of these will actually do anything, except produce the string and then throw it away. So let's fix that:
print("I want a " + add_bar("chocolate") + ".")