
Python 3.14.0 was released on 7 October 2025 and it brings several useful features and improvements such as:
In Python 3.14, annotations are now evaluated lazily by default. This means that the evaluation of type hints is postponed until they are actually needed, which can help improve performance and reduce memory usage.
It means following will run without error:
def func(arg: Undefined):
pass
func("bar")
Previously, this would raise a NameError because
Undefined was not defined at the time of function
definition.
In addition to that, Python 3.14 also introduces a new
annotationlib module that provides utilities for working
with annotations.
In order to retrieve the annotations in an above example you can use:
from annotationlib import get_annotations, Format
def func(arg: Undefined):
pass
get_annotations(func, format=Format.FORWARDREF)
# Output: {'arg': ForwardRef('Undefined', owner=<function func at 0x102ad33d0>)}
If you don’t pass format argument as Format.FORWARDREF
then it will fail with NameError, because it will try to
evaluate the annotation.
Thanks to Lazy evaluation of the annotations, it is no longer necessary to enclose annotations in strings if they contain forward references.
What happened here, is basically Python 3.14 implemented PEP 768, by introducing zero-overhead debugging interface, which allows you to sneak into a running Python process, without prior instrumentation. This is basically a game changer and will allow for the creation of a whole new class of debugging tools for Python.
There is basically a new sys.remote_exec function that
allows you to execute a script in the context of another Python
process.
Let’s demonstrate how it works.
You have the following super-critical application running on prod:
# critical_app.py
from time import sleep
i=0
while 1:
i += 1
sleep(1)
Using following external script you can send out python code to be executed in the context of the specified process:
# safe_ext_debugger_simple.py
import sys
import uuid
target_pid = 77801 # <-- Replace with the actual PID of the target process
with open(f"/tmp/{uuid.uuid4()}.py", "w") as f:
f.write("print('Hello from another process!')\n")
sys.remote_exec(target_pid, f.name)
Before running the above script, you need to replace
target_pid variable with the actual PID of the target
process.
You can find the PID of the target process using ps
command:
# If the script is named critical_app.py
ps aux | grep critical_app.py
After running the above script, it will send the code to be executed in the specified process. In the target process, you will see the following:
Hello from another process!
You might be wondering, why would I put the `i`` variable ? I guess you already have an idea where this is going.
We can actually retrieve the value of i variable,
without touching the target process at all.
Following script will do the job (Don’t forget to replace
target_pid):
# safe_ext_debugger.py
import sys
import uuid
script = """
import sys
for thread_id, frame in sys._current_frames().items():
current_frame = frame
frame_num = 0
while current_frame is not None:
if 'i' in current_frame.f_locals:
value = current_frame.f_locals['i']
func_name = current_frame.f_code.co_name
filename = current_frame.f_code.co_filename
lineno = current_frame.f_lineno
print(f"Frame {frame_num}: {func_name}() at {filename}:{lineno}")
print(f"\\n i = {repr(value)}")
print(f"\\n All locals in this frame:")
for key, val in sorted(current_frame.f_locals.items()):
# Truncate long values
val_str = repr(val)
if len(val_str) > 100:
val_str = val_str[:97] + "..."
print(f" {key} = {val_str}")
print()
current_frame = current_frame.f_back
frame_num += 1
"""
target_pid = 77801 # <-- Replace with the actual PID of the target process
with open(f"/tmp/{uuid.uuid4()}.py", "w") as f:
f.write(script)
sys.remote_exec(target_pid, f.name)
Basically what this script does is:
sys._current_frames() to get the current stack
frames of all threads in the target process.i is present in the local variables of the frame.When you run the above script, you will get the following output:
Frame 1: <module>() at /***/critical_app.py:6
i = 1096
All locals in this frame:
__builtins__ = <module 'builtins' (built-in)>
__cached__ = None
__doc__ = None
__file__ = '/***/critical_app.py'
__loader__ = <_frozen_importlib_external.SourceFileLoader object at 0x1025ec3b0>
__name__ = '__main__'
__package__ = None
__spec__ = None
i = 1096
sleep = <built-in function sleep>
As you can see, we were able to retrieve the value of i
variable from the target process without touching it at all.
Template strings (t-strings) as the documentation suggests, are a new way for custom string processing. The syntax is pretty much similar to f-strings.
company = "Codeff"
print(t"Company name is {company}")
# Output: Template(strings=('Company name is ', ''), interpolations=(Interpolation('Codeff', 'company', None, ''),))
The output is not so representative, because it’s not the way you would normally use it.
Your code should be designed to use the Template object
returned by the t-string. And in order to be able to do that, there is a
new stringlib module introduced under string
standard library.
Let’s see how it works:
from string.templatelib import Interpolation
company = "Codeff"
template=t"Company name is {company}"
def custom_processing(template):
""" Some explanation of the attributes of Template object.
interpolations consist of Interpolation object which has following attributes:
- conversion - Convert *obj* using formatted string literal semantics.
- expression - The original expression used in the template.
- format_spec - The format specification for the value.
- value - The evaluated value of the expression.
strings consist of literal string parts.
values consist of evaluated values of expressions.
"""
interpolations = template.interpolations
strings = template.strings
values = template.values
print (isinstance(interpolations[0], Interpolation)) # Just for demonstration the usage of Interpolation class
for interpolation in interpolations:
print (f"{interpolation.expression=}")
print (f"{interpolation.conversion=}")
print (f"{interpolation.format_spec=}")
print (f"{interpolation.value=}")
for string in strings:
print (f"{string=}")
for value in values:
print (f"{value=}")
custom_processing(template)
The output will be:
True
interpolation.expression='company'
interpolation.conversion=None
interpolation.format_spec=''
interpolation.value='Codeff'
string='Company name is '
string=''
value='Codeff'
As you can see, you can access the individual parts of the template string and process them as needed.
For more information, please refer to the official documentation and the PEP 750.
There are lots more interesting features and improvements in Python
3.14, but those that I shared with you in this article are the ones that
I found most interesting and mind-blowing (especially
sys.remote_exec).
I hope you found this article informative and helpful.
If you have any questions or comments, please feel free to comment them in the LinkedIn post linked below.
Found this helpful? 💙📤 Spread the word! 👇