Codeff Tech Blog

Back↩︎

Mind-Blowing Features in Python 3.14

Python 3.14.0 was released on 7 October 2025 and it brings several useful features and improvements such as:

Deferred or Lazy Evaluation of Annotations

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.

Safe External Debugger Interface

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:

  1. It creates a temporary Python script that will be executed in the context of the target process.
  2. It uses sys._current_frames() to get the current stack frames of all threads in the target process.
  3. It iterates over each frame and checks if the variable i is present in the local variables of the frame.
  4. Then the rest is already self-explanatory.

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

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.

Summary

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! 👇