The "proper" way would be to do use ast.parse, as @blhsing recommended. Still, in many cases this may be an overkill. In your simple case and similar cases, this is what I would do:
import re
expression = "3*x + function_x(y)"
values = {'x': 2, 'y': 5} # feel free to use a subset of `globals()` or `locals()` here, to reflect your workspace
# Substitute variable symbols with their values
for var, val in values.items():
pattern = r'\b' + re.escape(var) + r'\b'
expression = re.sub(pattern, str(val), expression)
assert(expression == "3*2 + function_x(5)")
The r'\b' pattern ensures that you only match whole-word occurences, in case a variable exists as a substring of another variable or function/built-in name etc (like x here exists in function_x).
The re.escape() function is necessary when you have variable symbols that contain special characters that have a special meaning in regular expressions. For example, the variable symbol itself could contain a special character, such as a .: y.z.