
Most deprecated APIs do not break the day they are deprecated. They break a year or two later, on the release that finally removes them, and by then nobody remembers the warning. In 2026 that bill came due across most of the modern stack at once. TypeScript 7 turned every option deprecated in 6.0 into a hard error. Python 3.15 removed a batch of APIs, Node.js 26 moved two dozen deprecations to end-of-life, and Lambda and GitHub Actions retired Node 20. This tracker is for developers who maintain a codebase older than a year and want to know what is already broken, what breaks next, and how to find it before CI does. Every TypeScript, Python and Node claim below comes from running the old code on the old and new release, with the raw output pasted.
How These Deprecated APIs Were Checked
Nothing in the TypeScript, Python and Node.js sections is quoted from memory. Each deprecated API got a probe: a file of a few lines that uses it. Each probe ran on the last release before the removal and on the current one. The Next.js, Lambda and GitHub Actions sections are read from the vendor’s own pages on the date below, because they need a deployed app or an account to probe.
Checked 2026-10-16
Windows 11 Pro 25H2 (build 26200.9711), Git Bash 5.2.37, GNU grep 3.0, curl 8.12.1
Node.js 24.22.0 and 26.12.0, official win-x64 zip builds
TypeScript 5.9.3, 6.0.3 and 7.0.2 from npm, run on Node.js 24.22.0
Python 3.14.8 and 3.15.0, official embeddable amd64 zips
nodejs/node doc/api/deprecations.md at tags v24.0.0 and v26.12.0
Next.js upgrade guide for version 16, page dated 2026-10-15 (latest release 16.4.2)
AWS Lambda runtimes page and GitHub changelog, read 2026-10-16
Next scheduled re-check: 2027-01-16, or when Node 26 enters LTS on 2026-10-28
Every probe reads or compiles a fixed file, so the result does not vary between runs, and one run is reported. Two limitations matter. First, a probe proves that one usage breaks. It does not prove that every usage of the same API breaks the same way. Second, the embeddable Python build disables site, which adds one extra sentence to one error message below. The behavior itself matches a normal install.
Which Deprecated APIs Break in 2026?
These are the deprecated APIs that stopped working, or got a hard date, between January and October 2026. The status column is what happened when the probe ran on the current release, not what the changelog predicted.
| Stack | Deprecated API or setting | Removed in | Status on the current release |
|---|---|---|---|
| TypeScript | baseUrl, target: es5, moduleResolution: node, module: amd/umd/system/none, outFile, downlevelIteration, esModuleInterop: false | 7.0 (2026-07-08) | Compile error, exit 1 |
| TypeScript | "ignoreDeprecations": "6.0" | 7.0 | No longer silences anything |
| Python | glob.glob1, platform.java_ver, PurePath.is_reserved, sre_compile, co_lnotab, CGIHTTPRequestHandler, typing.no_type_check_decorator | 3.15 (2026-10-09) | AttributeError or ImportError |
| Python | NamedTuple keyword syntax, empty TypedDict("TD"), files(package=...), strptime with %d and no year | 3.15 | TypeError or ValueError |
| Python | Python 3.10 itself | End of life 2026-10-01 | No more security fixes |
| Node.js | fs.rmdir(path, { recursive: true }), assert.CallTracker | 26 (2026-05-05) | TypeError |
| Node.js | process.on('multipleResolves'), multi-argument assert.fail() | 26 | Runs, but silently does something else |
| Node.js | Node.js 20 itself | End of life 2026-04-30 | No more security fixes |
| Next.js | middleware.ts, next/legacy/image, images.domains, one-argument revalidateTag | Deprecated in 16 | Still works, removal pending |
| Next.js | next lint, AMP, serverRuntimeConfig, publicRuntimeConfig | 16 | Removed |
| AWS Lambda | nodejs20.x, ruby3.2, provided.al2, python3.10, dotnet8 | Deprecated 2026 | Updates blocked from 2027-08-31 |
| GitHub Actions | Actions declaring using: node20 | Runners, 2026-09-23 | Node 20 removed from runners |
The rest of this post shows the receipts for each row, then a single script that finds every one of them in your own repository.
Why Deprecated Code Survives Until the Day It Breaks
Deprecated code survives because the warning rarely reaches anyone. Python is the clearest case. By default, Python shows a DeprecationWarning only when it fires in the script you ran directly, not in a module that script imported. Since real code lives in imported modules, the warning almost never prints outside a test runner.
The probe below puts a deprecated call in a helper module, then runs it three ways:
# helpers.py
import glob
def list_py(folder):
return glob.glob1(folder, "*.py")
# main.py
import helpers
print(helpers.list_py("."))
== Python 3.14.8
$ python main.py
['helpers.py', 'main.py']
$ python -W error::DeprecationWarning main.py
DeprecationWarning: glob.glob1 is deprecated and will be removed in Python 3.15. Use glob.glob and pass a directory to its root_dir argument
== Python 3.15.0
$ python main.py
AttributeError: module 'glob' has no attribute 'glob1'. Did you mean '.glob' instead of '.glob1'?
$ python -W error::DeprecationWarning main.py
AttributeError: module 'glob' has no attribute 'glob1'. Did you mean '.glob' instead of '.glob1'?
On 3.14.8, the plain run printed the result and nothing else. So a service running this code in production for the whole 3.13 and 3.14 cycle never logged a single warning. Then the first run on 3.15.0 crashed. TypeScript and Node behave better here, since both print their warnings by default. However, both still let a build pass, so nothing forces the fix until the removal lands.
TypeScript 7: The tsconfig Options That Stopped Compiling
TypeScript 6.0 shipped in March 2026 as a bridge release. It deprecated a long list of compiler options and let you silence the errors with "ignoreDeprecations": "6.0". TypeScript 7.0, the Go-based native compiler, became latest on npm on 2026-07-08 and removed all of them. The probe script below compiles one tiny project per option on each version:
#!/usr/bin/env bash
# run-ts.sh: compiles one tiny project per tsconfig option on each TypeScript version
# and prints the diagnostic code and first line. Usage: ./run-ts.sh <ts-dir>...
probe() { # name, compilerOptions JSON fragment
mkdir -p "case/$1/src"
printf '{ "compilerOptions": { %s, "noEmit": true }, "include": ["src"] }\n' "$2" > "case/$1/tsconfig.json"
printf 'export const answer: number = 42;\n' > "case/$1/src/index.ts"
}
probe target-es5 '"target": "es5"'
probe downlevelIteration '"downlevelIteration": true'
probe moduleResolution-node '"module": "commonjs", "moduleResolution": "node"'
probe moduleResolution-classic '"moduleResolution": "classic"'
probe module-amd '"module": "amd"'
probe module-umd '"module": "umd"'
probe module-system '"module": "system"'
probe module-none '"module": "none"'
probe outFile '"module": "amd", "outFile": "out.js"'
probe baseUrl '"baseUrl": "."'
probe esModuleInterop-false '"esModuleInterop": false'
probe allowSyntheticDefaultImports-false '"allowSyntheticDefaultImports": false'
probe alwaysStrict-false '"alwaysStrict": false'
probe importsNotUsedAsValues '"importsNotUsedAsValues": "remove"'
probe preserveValueImports '"preserveValueImports": true'
probe keyofStringsOnly '"keyofStringsOnly": true'
probe suppressImplicitAnyIndexErrors '"suppressImplicitAnyIndexErrors": true'
probe charset '"charset": "utf8"'
probe out '"out": "out.js"'
probe noImplicitUseStrict '"noImplicitUseStrict": true'
probe ignoreDeprecations-6 '"baseUrl": ".", "ignoreDeprecations": "6.0"'
probe control-modern '"target": "es2022", "module": "nodenext"'
for ts in "$@"; do
v=$(node "$ts/node_modules/typescript/bin/tsc" -v 2>/dev/null || "$ts/node_modules/.bin/tsc" -v)
echo "== $v"
for d in case/*/; do
n=$(basename "$d")
out=$("$ts/node_modules/.bin/tsc" -p "$d" 2>&1); code=$?
first=$(echo "$out" | grep -m1 -oE 'error TS[0-9]+: .*' | cut -c1-150)
printf '%-36s exit=%s %s\n' "$n" "$code" "${first:-clean}"
done
done
Each argument is a folder holding one TypeScript version, installed there with npm i typescript@<version>. The output, with long lines cut at 150 characters by the script itself:
./run-ts.sh ts6.0.3 ts7.0.2
== Version 6.0.3
allowSyntheticDefaultImports-false exit=2 error TS5107: Option 'allowSyntheticDefaultImports=false' is deprecated and will stop functioning in TypeScript 7.0. Specify compilerOption '"ignoreDe
alwaysStrict-false exit=2 error TS5107: Option 'alwaysStrict=false' is deprecated and will stop functioning in TypeScript 7.0. Specify compilerOption '"ignoreDeprecations": "6.
baseUrl exit=2 error TS5101: Option 'baseUrl' is deprecated and will stop functioning in TypeScript 7.0. Specify compilerOption '"ignoreDeprecations": "6.0"' to sile
charset exit=2 error TS5102: Option 'charset' has been removed. Please remove it from your configuration.
control-modern exit=0 clean
downlevelIteration exit=2 error TS5101: Option 'downlevelIteration' is deprecated and will stop functioning in TypeScript 7.0. Specify compilerOption '"ignoreDeprecations": "6.
esModuleInterop-false exit=2 error TS5107: Option 'esModuleInterop=false' is deprecated and will stop functioning in TypeScript 7.0. Specify compilerOption '"ignoreDeprecations":
ignoreDeprecations-6 exit=0 clean
importsNotUsedAsValues exit=0 clean
keyofStringsOnly exit=2 error TS5102: Option 'keyofStringsOnly' has been removed. Please remove it from your configuration.
module-amd exit=2 error TS5107: Option 'module=AMD' is deprecated and will stop functioning in TypeScript 7.0. Specify compilerOption '"ignoreDeprecations": "6.0"' to s
module-none exit=2 error TS5107: Option 'module=None' is deprecated and will stop functioning in TypeScript 7.0. Specify compilerOption '"ignoreDeprecations": "6.0"' to
module-system exit=2 error TS5107: Option 'module=System' is deprecated and will stop functioning in TypeScript 7.0. Specify compilerOption '"ignoreDeprecations": "6.0"' t
module-umd exit=2 error TS5107: Option 'module=UMD' is deprecated and will stop functioning in TypeScript 7.0. Specify compilerOption '"ignoreDeprecations": "6.0"' to s
moduleResolution-classic exit=2 error TS5107: Option 'moduleResolution=classic' is deprecated and will stop functioning in TypeScript 7.0. Specify compilerOption '"ignoreDeprecations
moduleResolution-node exit=2 error TS5107: Option 'moduleResolution=node10' is deprecated and will stop functioning in TypeScript 7.0. Specify compilerOption '"ignoreDeprecations"
noImplicitUseStrict exit=2 error TS5102: Option 'noImplicitUseStrict' has been removed. Please remove it from your configuration.
out exit=2 error TS5102: Option 'out' has been removed. Please remove it from your configuration.
outFile exit=2 error TS5107: Option 'module=AMD' is deprecated and will stop functioning in TypeScript 7.0. Specify compilerOption '"ignoreDeprecations": "6.0"' to s
preserveValueImports exit=2 error TS5102: Option 'preserveValueImports' has been removed. Please remove it from your configuration.
suppressImplicitAnyIndexErrors exit=2 error TS5102: Option 'suppressImplicitAnyIndexErrors' has been removed. Please remove it from your configuration.
target-es5 exit=2 error TS5107: Option 'target=ES5' is deprecated and will stop functioning in TypeScript 7.0. Specify compilerOption '"ignoreDeprecations": "6.0"' to s
== Version 7.0.2
allowSyntheticDefaultImports-false exit=1 error TS5108: Option 'allowSyntheticDefaultImports=false' has been removed. Please remove it from your configuration.
alwaysStrict-false exit=1 error TS5108: Option 'alwaysStrict=false' has been removed. Please remove it from your configuration.
baseUrl exit=1 error TS5102: Option 'baseUrl' has been removed. Please remove it from your configuration.
charset exit=1 error TS5023: Unknown compiler option 'charset'.
control-modern exit=0 clean
downlevelIteration exit=1 error TS5102: Option 'downlevelIteration' has been removed. Please remove it from your configuration.
esModuleInterop-false exit=1 error TS5108: Option 'esModuleInterop=false' has been removed. Please remove it from your configuration.
ignoreDeprecations-6 exit=1 error TS5102: Option 'baseUrl' has been removed. Please remove it from your configuration.
importsNotUsedAsValues exit=1 error TS5023: Unknown compiler option 'importsNotUsedAsValues'.
keyofStringsOnly exit=1 error TS5023: Unknown compiler option 'keyofStringsOnly'.
module-amd exit=1 error TS5095: Option 'bundler' can only be used when 'module' is set to 'preserve', 'commonjs', or 'es2015' or later.
module-none exit=1 error TS6046: Argument for '--module' option must be: 'commonjs', 'es6', 'es2015', 'es2020', 'es2022', 'esnext', 'node16', 'node18', 'node20', 'nodene
module-system exit=1 error TS5095: Option 'bundler' can only be used when 'module' is set to 'preserve', 'commonjs', or 'es2015' or later.
module-umd exit=1 error TS5095: Option 'bundler' can only be used when 'module' is set to 'preserve', 'commonjs', or 'es2015' or later.
moduleResolution-classic exit=1 error TS5108: Option 'moduleResolution=Classic' has been removed. Please remove it from your configuration.
moduleResolution-node exit=1 error TS5108: Option 'moduleResolution=node10' has been removed. Please remove it from your configuration.
noImplicitUseStrict exit=1 error TS5023: Unknown compiler option 'noImplicitUseStrict'.
out exit=1 error TS5023: Unknown compiler option 'out'.
outFile exit=1 error TS5095: Option 'bundler' can only be used when 'module' is set to 'preserve', 'commonjs', or 'es2015' or later.
preserveValueImports exit=1 error TS5023: Unknown compiler option 'preserveValueImports'.
suppressImplicitAnyIndexErrors exit=1 error TS5023: Unknown compiler option 'suppressImplicitAnyIndexErrors'.
target-es5 exit=1 error TS5108: Option 'target=ES5' has been removed. Please remove it from your configuration.
Reading the TypeScript 7 Results
Three details in that output need a closer look. First, the module-amd lines on 7.0.2 complain about bundler, not AMD. That happens because 7.0 resolves modules with bundler by default, and bundler rejects AMD before the removal check gets a turn. The full output for that case shows both errors:
tsc -p case/module-amd # with TypeScript 7.0.2
case/module-amd/tsconfig.json(1,3): error TS5095: Option 'bundler' can only be used when 'module' is set to 'preserve', 'commonjs', or 'es2015' or later.
case/module-amd/tsconfig.json(1,34): error TS5108: Option 'module=AMD' has been removed. Please remove it from your configuration.
Second, ignoreDeprecations: "6.0" gave a clean build on 6.0.3 and the same baseUrl error as everything else on 7.0.2. Many teams added that line to get through the 6.0 upgrade quickly. In practice, it only bought time until July. Third, importsNotUsedAsValues: "remove" compiled with no diagnostic at all on 6.0.3, because "remove" was the old default value. On 7.0.2 it is an unknown option. So a config that looks clean on 6.0 can still fail on 7.0.
Also notice the exit codes. The same option errors exit with 2 on 6.0.3 and with 1 on 7.0.2, while the rootDir error further down exits with 2 on both. So any CI step that treats one specific nonzero code as “type errors, keep going” needs a second look.
The Defaults That Changed Under Untouched Code
Removed options are the visible half. The quieter half is that TypeScript 6.0 changed defaults, so a project with an almost empty tsconfig breaks too. This probe uses { "compilerOptions": { "noEmit": true } }, with @types/node installed and this source file:
// Untouched pre-6.0 code: relies on @types/node being picked up automatically
// and on implicit any being allowed.
export function port(fallback) {
return Number(process.env.PORT ?? fallback);
}
== TypeScript 5.9.3
exit=0
== TypeScript 6.0.3
src/index.ts(3,22): error TS7006: Parameter 'fallback' implicitly has an 'any' type.
src/index.ts(4,17): error TS2591: Cannot find name 'process'. Do you need to install type definitions for node? Try `npm i --save-
exit=2
== TypeScript 7.0.2
src/index.ts(3,22): error TS7006: Parameter 'fallback' implicitly has an 'any' type.
src/index.ts(4,17): error TS2591: Cannot find name 'process'. Do you need to install type definitions for node? Try `npm i --save-
exit=1
Two defaults moved here. strict is now on, which produces the implicit any error. In addition, types now defaults to an empty list, so @types/node is no longer picked up automatically, even though it sits in node_modules. A third default bites any project that sets outDir. Here is a config without rootDir, saved as tsconfig.norootdir.json and compiled with tsc -p tsconfig.norootdir.json:
{
"compilerOptions": {
"target": "es2022",
"module": "nodenext",
"outDir": "./dist",
"types": ["node"],
"strict": true
},
"include": ["src"]
}
== TypeScript 5.9.3
exit=0
== TypeScript 6.0.3
tsconfig.norootdir.json(5,5): error TS5011: The common source directory of 'tsconfig.norootdir.json' is './src'. The 'rootDir' setting must be explicitly set to this or another path to adjust your output's file layout.
Visit https://aka.ms/ts6 for migration information.
exit=2
== TypeScript 7.0.2
tsconfig.norootdir.json(5,5): error TS5011: The common source directory of 'tsconfig.norootdir.json' is './src'. The 'rootDir' setting must be explicitly set to this or another path to adjust your output's file layout.
Visit https://aka.ms/ts6 for migration information.
exit=2
A tsconfig That Compiles on TypeScript 6 and 7
For a Node.js service, this config compiled with exit 0 on both 6.0.3 and 7.0.2, and node dist/index.js printed 3000:
{
"compilerOptions": {
"target": "es2022",
"module": "nodenext",
"rootDir": "./src",
"outDir": "./dist",
"types": ["node"],
"strict": true
},
"include": ["src"]
}
With nodenext, relative imports need the .js extension, as in import { port } from './lib/port.js', and the package needs "type": "module". For a frontend app where Vite or another bundler emits the JavaScript, the replacement for baseUrl is a paths entry that starts with ./. This one also passed on both versions:
{
"compilerOptions": {
"target": "es2022",
"module": "preserve",
"moduleResolution": "bundler",
"types": ["node"],
"strict": true,
"noEmit": true,
"paths": { "@/*": ["./src/*"] }
},
"include": ["src"]
}
If strict produces hundreds of errors in an old codebase, set "strict": false explicitly for now. That is still allowed. Only alwaysStrict: false is gone, and it controls the emitted "use strict" directive, not type checking.
Python 3.15: Removed APIs and the New UTF-8 Default
Python 3.15.0 shipped on 2026-10-09. The What’s New in Python 3.15 page lists the removals, and most of these deprecated APIs first warned in 3.13. Each probe below is a two- or three-line file that uses one removed API. The runner prints the first warning, else the exception, else the printed result:
#!/usr/bin/env bash
# run-py.sh: runs every probe on each Python with warnings enabled, and prints the
# exit code plus the first warning, else the exception, else the printed result.
# Usage: ./run-py.sh <python>...
for py in "$@"; do
echo "== $("$py" -V)"
for f in *.py; do
out=$("$py" -W default "$f" 2>&1); code=$?
line=$(echo "$out" | grep -m1 -oE '[A-Za-z]*Warning: .*')
[ -z "$line" ] && line=$(echo "$out" | grep -vE '^Traceback|^ ' | tail -1)
printf '%-28s exit=%s %s\n' "${f%.py}" "$code" "$(echo "$line" | cut -c1-118)"
done
done
Two of the probes, as examples of how small they are:
# namedtuple-kwargs.py
from typing import NamedTuple
Point = NamedTuple("Point", x=int, y=int)
print(Point(1, 2))
# strptime-no-year.py
from datetime import datetime
print(datetime.strptime("03-15", "%m-%d"))
The output, with two long Windows temp paths trimmed and marked:
== Python 3.14.8
ast-missing-field exit=0 DeprecationWarning: Name.__init__ missing 1 required positional argument: 'id'. This will become an error in Python 3.
asyncio-iscoroutinefunction exit=0 DeprecationWarning: 'asyncio.iscoroutinefunction' is deprecated and slated for removal in Python 3.16; use inspect.isc
asyncio-policy exit=0 DeprecationWarning: 'asyncio.DefaultEventLoopPolicy' is deprecated and slated for removal in Python 3.16
bytestring-import exit=0 <class 'collections.abc.ByteString'>
cgi-handler exit=0 ['/cgi-bin', '/htbin']
co-lnotab exit=0 DeprecationWarning: co_lnotab is deprecated, use co_lines instead.
glob1 exit=0 DeprecationWarning: glob.glob1 is deprecated and will be removed in Python 3.15. Use glob.glob and pass a directory to
is-reserved exit=0 DeprecationWarning: pathlib.PurePath.is_reserved() is deprecated and scheduled for removal in Python 3.15. Use os.path
java-ver exit=0 DeprecationWarning: 'java_ver' is deprecated and slated for removal in Python 3.15
namedtuple-kwargs exit=0 DeprecationWarning: Creating NamedTuple classes using keyword arguments is deprecated and will be disallowed in Python
no-type-check-decorator exit=0 <function no_type_check_decorator at 0x000001D0F2550880>
profile-module exit=0 profile
resources-package-kw exit=0 DeprecationWarning: First parameter to files is renamed to 'anchor'
sre-compile exit=0 DeprecationWarning: module 'sre_compile' is deprecated
strptime-no-year exit=0 DeprecationWarning: Parsing dates involving a day of month without a year specified is ambiguous
typeddict-empty exit=0 DeprecationWarning: Failing to pass a value for the 'fields' parameter is deprecated and will be disallowed in Python
utf8-default exit=0 cp1252 -> 'caf\xc3\xa9, na\xc3\xafve, Z\xc3\xbcrich'
== Python 3.15.0
ast-missing-field exit=1 TypeError: ast.Name.__init__ missing 1 required positional argument: 'id'
asyncio-iscoroutinefunction exit=0 DeprecationWarning: 'asyncio.iscoroutinefunction' is deprecated and slated for removal in Python 3.16; use inspect.isc
asyncio-policy exit=0 DeprecationWarning: 'asyncio.DefaultEventLoopPolicy' is deprecated and slated for removal in Python 3.16
bytestring-import exit=0 DeprecationWarning: 'collections.abc.ByteString' is deprecated and slated for removal in Python 3.17
cgi-handler exit=1 ImportError: cannot import name 'CGIHTTPRequestHandler' from 'http.server' ([temp path trimmed]
co-lnotab exit=1 AttributeError: 'code' object has no attribute 'co_lnotab'. Did you mean '.co_linetable' instead of '.co_lnotab'?
glob1 exit=1 AttributeError: module 'glob' has no attribute 'glob1'. Did you mean '.glob' instead of '.glob1'?
is-reserved exit=1 AttributeError: 'PureWindowsPath' object has no attribute 'is_reserved'
java-ver exit=1 AttributeError: module 'platform' has no attribute 'java_ver'. Did you mean '.mac_ver' instead of '.java_ver'?
namedtuple-kwargs exit=1 TypeError: NamedTuple() got an unexpected keyword argument 'x'
no-type-check-decorator exit=1 ImportError: cannot import name 'no_type_check_decorator' from 'typing' ([temp path trimmed]
profile-module exit=0 DeprecationWarning: The profile module is deprecated and will be removed in Python 3.17. Use profiling.tracing (or cPr
resources-package-kw exit=1 TypeError: files() got an unexpected keyword argument 'package'
sre-compile exit=1 ModuleNotFoundError: No module named 'sre_compile'. Site initialization is disabled, did you forget to add the site-pa
strptime-no-year exit=1 ValueError: Day of month directive '%d' may not be used without a year directive. Parsing dates involving a day of mon
typeddict-empty exit=1 TypeError: TypedDict() missing 1 required positional argument: 'fields'
utf8-default exit=0 utf-8 -> 'caf\xe9, na\xefve, Z\xfcrich'
Twelve probes ran on 3.14.8 and failed on 3.15.0. The “Site initialization is disabled” hint on sre_compile comes from the embeddable build mentioned earlier. A regular install prints only the first sentence.
Two Removals That Never Warned
Most of the 3.14.8 lines show a DeprecationWarning. Two do not, even with warnings enabled. Importing CGIHTTPRequestHandler and typing.no_type_check_decorator printed nothing on 3.14.8, and both raise ImportError on 3.15.0. A separate check confirmed that both warn when called, not when imported. As a result, a module that imports them but rarely calls them can pass a full test run with -W error and still fail at import time on 3.15. That case needs a text search, not a warnings filter, which is what the sweep script later in this post does.
The strptime change is the one most likely to hit application code rather than tooling. Parsing a birthday or a recurring date with "%m-%d" now raises ValueError. The fix is to add a year to both the format and the string, for example datetime.strptime(f"2000-{value}", "%Y-%m-%d") when the year does not matter. Pick a leap year such as 2000, or February 29 fails.
open() Without an Encoding Reads Differently on Windows
The last probe is not a removal, but it changes behavior with no error at all. Python 3.15 turns on UTF-8 mode by default, from PEP 686. The probe writes UTF-8 bytes and reads them back with a bare open():
# Writes UTF-8 bytes, then reads them back with open() and no encoding argument.
import pathlib, tempfile
p = pathlib.Path(tempfile.gettempdir()) / "utf8-probe.txt"
p.write_bytes("café, naïve, Zürich".encode("utf-8"))
with open(p) as f:
print(f.encoding, "->", ascii(f.read()))
On Windows, 3.14.8 decoded the file as cp1252 and returned mojibake. In contrast, 3.15.0 used UTF-8 and returned the right text. That is a fix for most apps. However, any Windows code that reads legacy files written in the local code page, such as CSV exports from older desktop tools, will now decode them wrongly. Pass encoding= explicitly in both directions. To find bare open() calls before upgrading, run your tests with python -X warn_default_encoding, which flags each one. On Linux and macOS, where the locale is almost always UTF-8 already, the visible effect is much smaller.
Pending Removal in Python 3.16 and 3.17
Looking ahead, the 3.15.0 probes also show what goes next. The asyncio policy system, including set_event_loop_policy and DefaultEventLoopPolicy, is scheduled for removal in 3.16, along with asyncio.iscoroutinefunction. After that, the profile module and collections.abc.ByteString go in 3.17. Of these deprecated APIs, the policy removal matters most, because it is how most code installs uvloop:
import asyncio
import uvloop
async def main() -> None:
...
# Before: asyncio.set_event_loop_policy(uvloop.EventLoopPolicy()); asyncio.run(main())
# After: works on 3.12 and later, no policy object involved
asyncio.run(main(), loop_factory=uvloop.new_event_loop)
The loop_factory argument has existed since Python 3.12, so this change is safe to make now on every supported version. If asyncio patterns in general are new territory, the guide to async programming in Python with asyncio and Trio covers the event loop model this change touches. Finally, Python 3.10 itself reached end of life on 2026-10-01, according to the Python version status page, so it receives no more security fixes.
Node.js 26: Deprecated APIs Moved to End-of-Life
Node.js tracks every deprecation in one file, doc/api/deprecations.md, with a stage for each: documentation-only, runtime warning, or end-of-life. Reading that file at two tags shows exactly what moved. The Node.js 24 breaking changes post already covers the 22 to 24 jump in depth, so this section sticks to what changed on the way to 26.
#!/usr/bin/env bash
# node-deps.sh: reads doc/api/deprecations.md at two Node.js tags and prints every
# deprecation whose stage changed, or that is new. Usage: ./node-deps.sh v24.0.0 v26.12.0
old=${1:-v24.0.0} new=${2:-v26.12.0}
stages() {
curl -sSf "https://raw.githubusercontent.com/nodejs/node/$1/doc/api/deprecations.md" | awk '
/^### DEP[0-9]+/ { if (id) print id "\t" type "\t" title
id=$2; sub(/:$/, "", id); title=$0; sub(/^### DEP[0-9]+: */, "", title); type="?" }
/^Type: / { if (type == "?") { type=$0; sub(/^Type: /, "", type); sub(/ \(.*/, "", type) } }
END { print id "\t" type "\t" title }' | sort
}
stages "$old" > "/tmp/dep-$old.tsv"; stages "$new" > "/tmp/dep-$new.tsv"
for t in "$old" "$new"; do
echo "== $t: $(wc -l < "/tmp/dep-$t.tsv") entries"
cut -f2 "/tmp/dep-$t.tsv" | sort | uniq -c | sort -rn
done
echo "== stage changes, $old -> $new"
join -t $'\t' -a2 -e NEW -o 0,1.2,2.2,2.3 "/tmp/dep-$old.tsv" "/tmp/dep-$new.tsv" |
awk -F'\t' '$2 != $3 { printf "%s %-18s -> %-18s %s\n", $1, $2, $3, $4 }'
Here is its output, with lines cut at 120 characters:
== v24.0.0: 188 entries
101 End-of-Life
41 Documentation-only
39 Runtime
4 Deprecation revoked
2 Application
1 Compile-time
== v26.12.0: 207 entries
125 End-of-Life
48 Documentation-only
24 Runtime
5 Deprecation revoked
3 Application
1 Revoked
1 Compile-time
== stage changes, v24.0.0 -> v26.12.0
DEP0031 Documentation-only -> Runtime `ecdh.setPublicKey()`
DEP0033 Documentation-only -> Revoked `EventEmitter.listenerCount()`
DEP0040 Runtime -> Application `node:punycode` module
DEP0063 Documentation-only -> End-of-Life `ServerResponse.prototype.writeHeader()`
DEP0077 Runtime -> End-of-Life `Module._debug()`
DEP0094 Runtime -> End-of-Life Using `assert.fail()` with more than one argument
DEP0112 Runtime -> End-of-Life `dgram` private APIs
DEP0118 Runtime -> End-of-Life `dns.lookup()` support for a falsy host name
DEP0123 Runtime -> End-of-Life setting the TLS ServerName to an IP address
DEP0125 Runtime -> End-of-Life `require('node:_stream_wrap')`
DEP0129 Runtime -> End-of-Life `ChildProcess._channel`
DEP0132 Runtime -> End-of-Life `worker.terminate()` with callback
DEP0135 Runtime -> End-of-Life `WriteStream.open()` and `ReadStream.open()` are internal
DEP0137 Runtime -> End-of-Life Closing fs.FileHandle on garbage collection
DEP0143 Runtime -> End-of-Life `Transform._transformState`
DEP0147 Runtime -> End-of-Life `fs.rmdir(path, { recursive: true })`
DEP0152 Runtime -> End-of-Life Extension PerformanceEntry properties
DEP0154 Runtime -> End-of-Life RSA-PSS generate key pair options
DEP0160 Runtime -> End-of-Life `process.on('multipleResolves', handler)`
DEP0163 Documentation-only -> Deprecation revoked `channel.subscribe(onMessage)`, `channel.unsubscribe(onMessage)`
DEP0170 Runtime -> End-of-Life Invalid port when using `url.parse()`
DEP0172 Runtime -> End-of-Life The `asyncResource` property of `AsyncResource` bound functions
DEP0173 Runtime -> End-of-Life the `assert.CallTracker` class
DEP0176 Runtime -> End-of-Life `fs.F_OK`, `fs.R_OK`, `fs.W_OK`, `fs.X_OK`
DEP0182 Runtime -> End-of-Life Short GCM authentication tags without explicit `authTagLength`
DEP0185 Runtime -> End-of-Life Instantiating `node:repl` classes without `new`
DEP0192 NEW -> Runtime `require('node:_tls_common')` and `require('node:_tls_wrap')`
DEP0193 NEW -> End-of-Life `require('node:_stream_*')`
DEP0194 NEW -> End-of-Life HTTP/2 priority signaling
DEP0195 NEW -> Documentation-only Instantiating `node:http` classes without `new`
DEP0196 NEW -> Documentation-only Calling `node:child_process` functions with `options.shell` as an empt
DEP0197 NEW -> Documentation-only `util.types.isNativeError()`
DEP0198 NEW -> Runtime Creating SHAKE-128 and SHAKE-256 digests without an explicit `options.
DEP0199 NEW -> Documentation-only `require('node:_http_*')`
DEP0200 NEW -> Documentation-only Closing fs.Dir on garbage collection
DEP0201 NEW -> Runtime Passing `options.type` to `Duplex.toWeb()`
DEP0202 NEW -> Documentation-only `Http1IncomingMessage` and `Http1ServerResponse` options of HTTP/2 ser
DEP0203 NEW -> Runtime Passing `CryptoKey` to `node:crypto` APIs
DEP0204 NEW -> Runtime `KeyObject.from()` with non-extractable `CryptoKey`
DEP0205 NEW -> Runtime `module.register()`
DEP0206 NEW -> Documentation-only Calling `digest()` on an already-finalized `Hmac` instance
DEP0208 NEW -> Documentation-only `Server.prototype._listen2`
DEP0209 NEW -> Documentation-only Using `AbortSignal` to dispose of resources
DEP0210 NEW -> Documentation-only `sqlite.DatabaseSync`
DEP0211 NEW -> Documentation-only `sqlite.StatementSync`
Between the two tags, the end-of-life count went from 101 to 125, while runtime warnings dropped from 39 to 24. In other words, more than half of the APIs that warned on Node 24 are gone on Node 26. Some of these moves happened in Node 25 rather than 26, since the comparison runs from the first 24 tag to the current 26 release.
What Old Node Code Does on 26.12.0
The probes covered seven of these deprecated APIs on both versions. Its runner prints the first deprecation code, else the error, else the printed result:
== Node v24.22.0
assert-calltracker exit=0 [DEP0173] DeprecationWarning: assert.CallTracker is deprecated.
assert-fail-multiarg exit=0 [DEP0094] DeprecationWarning: assert.fail() with more than one argument is deprecated. Please use assert.strictEqual() i
module-register exit=0 hooks registered
multiple-resolves exit=0 listener added
punycode exit=0 [DEP0040] DeprecationWarning: The `punycode` module is deprecated. Please use a userland alternative instead.
rmdir-recursive exit=0 [DEP0147] DeprecationWarning: In future versions of Node.js, fs.rmdir(path, { recursive: true }) will be removed. Use fs
shake-no-length exit=0 64
== Node v26.12.0
assert-calltracker exit=1 TypeError: assert.CallTracker is not a constructor
assert-fail-multiarg exit=0 ERR_ASSERTION 1
module-register exit=0 [DEP0205] DeprecationWarning: `module.register()` is deprecated. Use `module.registerHooks()` instead.
multiple-resolves exit=0 listener added
punycode exit=0 [DEP0040] DeprecationWarning: The `punycode` module is deprecated. Please use a userland alternative instead.
rmdir-recursive exit=1 TypeError [ERR_INVALID_ARG_VALUE]: The property 'options.recursive' is no longer supported. Received true
shake-no-length exit=0 [DEP0198] DeprecationWarning: Creating SHAKE128/256 digests without an explicit options.outputLength is deprecated.
Crashes are the harmless part, since they fail loudly. Instead, the two lines that matter more exited with 0. First, process.on('multipleResolves', ...) still registers on 26.12.0 without a word, but the event is end-of-life, so the handler never runs. Any monitoring built on it went quiet. Second, assert.fail(1, 2, undefined, '>') used to build the message 1 > 2. On 26.12.0 the extra arguments are ignored and the message is only 1. Test output still fails, but the failure message lost its meaning.
The fixes for the rest are short. Replace fs.rmdirSync(dir, { recursive: true }) with fs.rmSync(dir, { recursive: true, force: true }), and assert.CallTracker with mock.fn() from node:test. For SHAKE digests, pass { outputLength: 32 } to createHash('shake256', ...) to keep the same 64-character hex output shown above. module.register() moves to module.registerHooks(), which runs hooks synchronously on the main thread, so treat it as a port, not a rename. Separately, Node.js 20 reached end of life on 2026-04-30 according to the Node.js release schedule, and Node 26 becomes LTS on 2026-10-28.
Next.js 16: Deprecated Conventions That Still Compile
Next.js was not probed for this post. Its deprecated APIs still compile, while removed ones fail the build, and the Next.js 16 upgrade guide, dated 2026-10-15, separates removals from deprecations, and that split is what matters for planning:
| Item | Status in Next.js 16 | Replacement |
|---|---|---|
middleware.ts file and middleware export | Deprecated | proxy.ts exporting proxy, Node runtime only |
next/legacy/image | Deprecated | next/image |
images.domains | Deprecated | images.remotePatterns |
revalidateTag(tag) with one argument | Deprecated, TypeScript error | revalidateTag(tag, 'max') or updateTag(tag) |
next lint and the eslint config option | Removed | ESLint or Biome CLI |
AMP (next/amp, useAmp, amp config) | Removed | None |
serverRuntimeConfig, publicRuntimeConfig | Removed | Environment variables |
experimental.dynamicIO, experimental.useCache, experimental_ppr | Removed | cacheComponents |
unstable_rootParams | Removed | next/root-params |
The middleware rename has a catch the codemod cannot resolve for you. According to the guide, proxy always runs on the Node.js runtime. So middleware that depends on the edge runtime has to stay as middleware.ts for now, until Next.js ships the follow-up edge runtime guidance the guide promises. The async request API removal is the other big Next.js 16 break, and the Next.js 16 async request APIs migration post measures what old code does there.
AWS Lambda Runtimes Deprecated in 2026
Unlike deprecated APIs in code, a Lambda runtime deprecation is a date, not an exception, so it shows up in a dashboard rather than a stack trace. This table comes from the AWS Lambda runtimes page, read on 2026-10-16. It lists every runtime whose deprecation date falls in 2026:
| Runtime | Identifier | Deprecated | Block function create | Block function update |
|---|---|---|---|---|
| Ruby 3.2 | ruby3.2 | 2026-03-31 | 2027-07-29 | 2027-08-31 |
| Node.js 20 | nodejs20.x | 2026-04-30 | 2027-07-29 | 2027-08-31 |
| OS-only, Amazon Linux 2 | provided.al2 | 2026-07-31 | 2027-07-29 | 2027-08-31 |
| Python 3.10 | python3.10 | 2026-10-31 | 2027-07-29 | 2027-08-31 |
| .NET 8 | dotnet8 | 2026-11-10 | 2027-07-29 | 2027-08-31 |
Amazon Linux 2 itself reached end of life on 2026-06-30. According to the same page, the AL2-based runtimes still receive critical security patches until their own deprecation dates. The block dates sit well past the usual 30 and 60 days after deprecation, because AWS extended them after customer feedback. That extension is a trap in practice. A function on nodejs20.x deploys fine today, so nothing in CI pushes the upgrade, and the first sign of trouble is a failed hotfix deploy on 2027-08-31.
To list affected functions in one region, the AWS CLI can filter by runtime. This was not run for this post, since it needs an AWS account:
# Lists function names still on a deprecated runtime in the current region
aws lambda list-functions \
--query "Functions[?Runtime=='nodejs20.x' || Runtime=='python3.10'].[FunctionName,Runtime]" \
--output text
The cost side of moving runtimes, including when Lambda stops being the right home at all, is covered in Lambda vs Fargate vs EC2 cost.
GitHub Actions: Node 20 Left the Runners
JavaScript actions declare their runtime in action.yml with runs.using. GitHub’s Node 20 deprecation changelog, read on 2026-10-16, gives three dates. Runners started using Node 24 by default on 2026-06-16, and Node 20 was removed from the runners on 2026-09-23. Until removal, ACTIONS_ALLOW_USE_UNSECURE_NODE_VERSION=true kept Node 20 running. After 2026-09-23 it no longer has anything to fall back to. The changelog also notes that Node 24 does not run on macOS 13.4 or earlier, and that ARM32 self-hosted runners lose support.
This deadline mostly hits two places. The first is internal actions a team wrote years ago and never touched again. The second is workflows that pin an old major of a third-party action, such as an outdated setup-node or cache tag, to avoid surprises. Both need a version bump or a one-line using: node24 change. Separately, node-version: 20 in a setup-node step still installs Node 20 for your own build steps. It is unaffected by the runner change, but it targets a release that is past end of life. The GitHub Actions CI/CD for Node.js guide walks through a complete workflow if yours needs more than a version bump.
Flutter: 486 Deprecations Waiting for a Bulk Removal
Flutter is the exception in this list, because nothing large was removed in 2026. The Flutter breaking changes tracker counted the @Deprecated annotations in the framework at each release, and the count rose from 277 at 3.22 to 486 at 3.47.7, with no batch-removal guide since 3.22. That pile will not stay forever. When the next bulk removal arrives, it will remove more deprecated APIs at once than any earlier one, so clearing deprecation warnings now with dart fix --apply is cheaper than clearing them later.
Find Deprecated APIs in Your Own Repository
Changelogs tell you which deprecated APIs are gone. It does not tell you whether your code uses it. The script below searches a repository for every deprecated API in this post and prints the file, line and replacement. It uses only Bash and GNU grep, so it runs in Git Bash, macOS with GNU grep, Linux and CI images. Save the three blocks below, in order, as one file named deprecation-sweep.sh.
#!/usr/bin/env bash
# deprecation-sweep.sh: finds the APIs deprecated or removed in 2026 that this post covers.
# Usage: ./deprecation-sweep.sh [repo-dir] (default: current directory)
# Prints: [tag] file:line: matched text -> replacement
# Exit code: 1 if anything matched, so it can gate CI.
root=${1:-.}
skip=(--exclude-dir=node_modules --exclude-dir=.git --exclude-dir=.venv --exclude-dir=venv
--exclude-dir=dist --exclude-dir=build --exclude-dir=.next --exclude-dir=site-packages)
js=(--include=*.js --include=*.cjs --include=*.mjs --include=*.ts --include=*.cts
--include=*.mts --include=*.jsx --include=*.tsx)
q="[\"']" # either quote character
found=0
check() { # tag, comma-separated globs (or "js"), extended regex, replacement
local tag=$1 globs=$2 re=$3 fix=$4 inc=() g x
if [ "$globs" = js ]; then inc=("${js[@]}"); else
IFS=, read -ra g <<< "$globs"; for x in "${g[@]}"; do inc+=(--include="$x"); done
fi
while IFS= read -r hit; do
found=1
printf '[%s] %s -> %s\n' "$tag" "$(echo "$hit" | tr -s ' \t' ' ' | cut -c1-100)" "$fix"
done < <(grep -rnEI "${skip[@]}" "${inc[@]}" -e "$re" "$root" 2>/dev/null)
}
The second block holds the patterns, one check line per deprecated API. Adding a new one later is a single line:
# TypeScript 7.0: options deprecated in 6.0 are now hard errors
T='tsconfig*.json,jsconfig*.json'
check ts7 "$T" '"baseUrl"[[:space:]]*:' 'remove; make "paths" relative to the tsconfig'
check ts7 "$T" '"moduleResolution"[[:space:]]*:[[:space:]]*"(node|node10|classic)"' '"nodenext" or "bundler"'
check ts7 "$T" '"target"[[:space:]]*:[[:space:]]*"es(3|5)"' '"es2015" or later'
check ts7 "$T" '"module"[[:space:]]*:[[:space:]]*"(amd|umd|system|none)"' '"esnext" or "preserve" plus a bundler'
check ts7 "$T" '"(outFile|downlevelIteration)"[[:space:]]*:' 'remove; let the bundler handle it'
check ts7 "$T" '"(esModuleInterop|allowSyntheticDefaultImports|alwaysStrict)"[[:space:]]*:[[:space:]]*false' 'delete the line'
check ts7 "$T" '"ignoreDeprecations"[[:space:]]*:[[:space:]]*"(5|6)\.0"' 'ignored by 7.0; fix what it hides'
check ts7 "$T" '"(importsNotUsedAsValues|preserveValueImports)"' '"verbatimModuleSyntax": true'
# Python 3.15: removed
check py3.15 '*.py' '^[[:space:]]*(import|from)[[:space:]]+sre_(compile|parse|constants)' 'use re'
check py3.15 '*.py' 'glob\.glob[01][[:space:]]*\(' 'glob.glob(pattern, root_dir=dir)'
check py3.15 '*.py' 'java_ver[[:space:]]*\(' 'remove'
check py3.15 '*.py' '\.is_reserved[[:space:]]*\(' 'os.path.isreserved()'
check py3.15 '*.py' 'CGIHTTPRequestHandler' 'a WSGI or ASGI server'
check py3.15 '*.py' 'no_type_check_decorator' 'remove the decorator'
check py3.15 '*.py' 'co_lnotab' 'code.co_lines()'
check py3.15 '*.py' "NamedTuple[[:space:]]*\([[:space:]]*$q[A-Za-z_]+$q[[:space:]]*,[[:space:]]*[A-Za-z_]+[[:space:]]*=" 'class syntax'
check py3.15 '*.py' "TypedDict[[:space:]]*\([[:space:]]*$q[A-Za-z_]+$q[[:space:]]*(,[[:space:]]*None[[:space:]]*)?\)" 'TypedDict("TD", {})'
check py3.15 '*.py' 'files[[:space:]]*\([[:space:]]*package[[:space:]]*=' 'files(anchor=...) or positional'
check py3.15 '*.py' '\.load_module[[:space:]]*\(' 'exec_module()'
check py3.15 '*.py' 'strptime[[:space:]]*\(.*%d' 'review: %d with no year now raises'
check py3.15 '*.py' 'ast\.[A-Z][A-Za-z]*[[:space:]]*\([[:space:]]*\)' 'pass every required field'
# Python 3.16 and 3.17: still warning today, removal scheduled
check py3.16 '*.py' '(get|set)_event_loop_policy|EventLoopPolicy' 'asyncio.run(main(), loop_factory=...)'
check py3.16 '*.py' 'asyncio\.iscoroutinefunction' 'inspect.iscoroutinefunction'
check py3.17 '*.py' '^[[:space:]]*(import[[:space:]]+profile$|from[[:space:]]+profile[[:space:]]+import)' 'cProfile'
check py3.17 '*.py' '\bByteString\b' 'collections.abc.Buffer or bytes | bytearray'
# Node.js 26: end-of-life or runtime-deprecated
check node26 js 'rmdir(Sync)?[[:space:]]*\(.*recursive' 'fs.rm(path, { recursive: true, force: true })'
check node26 js 'CallTracker' 'mock.fn() from node:test'
check node26 js 'multipleResolves' 'remove; the event never fires'
check node26 js "module\.register[[:space:]]*\(|register[^}]*\}[[:space:]]*(=[[:space:]]*require\(|from)[[:space:]]*${q}(node:)?module$q" 'module.registerHooks()'
check node26 js "require\([[:space:]]*${q}punycode$q" 'url.domainToASCII()'
check node26 js 'fs\.(F|R|W|X)_OK\b' 'fs.constants.F_OK'
check node26 js "createHash\([[:space:]]*${q}shake(128|256)$q[[:space:]]*\)" 'pass { outputLength }'
check node26 js 'assert\.fail\([^)]*,[^)]*\)' 'assert.strictEqual() or assert.fail(message)'
The third block covers Next.js, CI and runtime identifiers, then sets the exit code:
# Next.js 16
check next16 'package.json' '"next lint' 'eslint . (codemod: next-lint-to-eslint-cli)'
check next16 'next.config.*' '(server|public)RuntimeConfig' 'process.env and NEXT_PUBLIC_ variables'
check next16 js "from[[:space:]]+${q}next/config$q" 'process.env'
check next16 js 'next/legacy/image' 'next/image'
check next16 'next.config.*' '\bdomains[[:space:]]*:' 'images.remotePatterns'
check next16 js 'revalidateTag\([^,()]*\)' "revalidateTag(tag, 'max') or updateTag(tag)"
check next16 js 'next/amp|useAmp|unstable_rootParams|experimental_ppr' 'removed in 16'
while IFS= read -r m; do
found=1; echo "[next16] $m -> proxy.ts exporting proxy (keep middleware only for edge)"
done < <(find "$root" \( -name node_modules -o -name .git -o -name .next \) -prune -o \
-type f \( -name 'middleware.ts' -o -name 'middleware.js' \) -print 2>/dev/null |
while IFS= read -r f; do [ -f "$(dirname "$f")/package.json" ] || [ -f "$(dirname "$f")/../package.json" ] && echo "$f"; done)
# CI, Lambda and container runtimes
check gha '*.yml,*.yaml' "using:[[:space:]]*$q?node(12|16|20)$q?[[:space:]]*$" 'runs.using: node24'
check gha '*.yml,*.yaml' 'ACTIONS_ALLOW_USE_UNSECURE_NODE_VERSION' 'remove; Node 20 left the runners'
check gha '*.yml,*.yaml' "node-version:[[:space:]]*$q?(16|18|20)(\.[0-9x.]*)?$q?[[:space:]]*$" 'node-version: 24'
check lambda '*.yml,*.yaml,*.json,*.tf,*.toml' "${q}?(nodejs(16|18|20)\.x|python3\.(8|9|10)|ruby3\.2|provided\.al2|dotnet[68])$q?[[:space:]]*,?[[:space:]]*$" 'nodejs24.x, python3.13+, provided.al2023, dotnet10'
check lambda js 'Runtime\.(NODEJS_(16|18|20)_X|PYTHON_3_(8|9|10)\b|RUBY_3_2|PROVIDED_AL2\b|DOTNET_[68]\b)' 'Runtime.NODEJS_24_X and friends'
check docker 'Dockerfile*,*.dockerfile' '^FROM[[:space:]]+(node:(16|18|20)|python:3\.(8|9|10))\b' 'node:24 or python:3.13+'
[ $found = 0 ] && echo "clean: nothing from the 2026 list found under $root"
exit $found
Testing the Sweep Against Known Old Code
A scanner that has never been tested is a guess. So the sweep ran against a fixture repository built from every probe in this post: the 16 TypeScript tsconfigs deprecated in 6.0, the Python and Node probe files, plus a small Next.js app, a workflow, an action.yml, a serverless.yml, a CDK stack and a Dockerfile, each using the deprecated form. It also ran against a clean project that uses the modern replacement for each one. The summary per tag:
./deprecation-sweep.sh fixture > sweep-fixture.txt; echo "exit=$?"
cut -d']' -f1 sweep-fixture.txt | tr -d '[' | sort | uniq -c
./deprecation-sweep.sh clean; echo "exit=$?"
exit=1
1 docker
3 gha
3 lambda
6 next16
7 node26
14 py3.15
2 py3.16
3 py3.17
18 ts7
clean: nothing from the 2026 list found under clean
exit=0
Every deprecated API in the fixture produced at least one hit, and the clean project produced none. A few of the 57 lines, to show the format:
[ts7] fixture/ts/ignoreDeprecations-6/tsconfig.json:1:{ "compilerOptions": { "baseUrl": ".", "ignoreDeprec -> ignored by 7.0; fix what it hides
[py3.15] fixture/py/strptime-no-year.py:2:print(datetime.strptime("03-15", "%m-%d")) -> review: %d with no year now raises
[node26] fixture/node/multiple-resolves.cjs:1:process.on('multipleResolves', () => {}); -> remove; the event never fires
[next16] fixture/web/middleware.ts -> proxy.ts exporting proxy (keep middleware only for edge)
[gha] fixture/action.yml:3: using: 'node20' -> runs.using: node24
[lambda] fixture/infra/serverless.yml:7: runtime: python3.10 -> nodejs24.x, python3.13+, provided.al2023, dotnet10
The fixture proves the patterns match the forms used in the probes. It does not prove they catch every spelling of the same call. For example, from glob import glob1 followed by a bare glob1(...) call slips past the glob.glob1 pattern, and some lines will be false positives, such as a strptime call that already includes a year. Treat the output as a review list, not a verdict.
Gate CI on Deprecations Before the Next Removal
The sweep finds code by text. The runtimes can also find it by behavior, and both together catch more than either alone. This job turns deprecation warnings into failures on the current releases:
# .github/workflows/deprecations.yml
name: deprecations
on: [pull_request]
jobs:
sweep:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v7
- name: Text sweep for 2026 removals
run: bash scripts/deprecation-sweep.sh .
- uses: actions/setup-node@v7
with:
node-version: 24
- name: Node tests with deprecations as errors
run: npm ci && NODE_OPTIONS=--throw-deprecation npm test
- uses: actions/setup-python@v7
with:
python-version: "3.15"
- name: Python tests with deprecations as errors
run: pip install -r requirements.txt && python -W error::DeprecationWarning -m pytest
The Python step uses -W error::DeprecationWarning, which also covers imported modules, unlike the default filter. Expect the first run to fail on warnings raised inside third-party packages too. When a dependency cannot be upgraded this week, add a narrow ignore filter for that one warning rather than dropping the flag for everything.
Real-World Scenario: A Monorepo Two Releases Behind
Consider a mid-sized monorepo maintained by a small team: a Next.js frontend, a few Node.js services on Lambda, and a Python data job, all last upgraded seriously in 2024. A developer bumps TypeScript to latest in a routine dependency update, and the build fails across every package at once, because the shared base tsconfig still carries baseUrl and moduleResolution: node. Rolling back is the quick fix. However, it only delays the problem, since the same deprecated APIs are waiting in every other part of the stack.
Running the sweep turns that one failure into a list. It typically surfaces a few dozen hits spread across all the layers: the shared tsconfig, nodejs20.x in the infrastructure files, a node20 internal action, an images.domains entry, and a handful of Python helpers on glob1 or the asyncio policy API. The useful move is to sort that list by deadline instead of by package. The GitHub Actions and TypeScript items are already broken. Next, the Lambda runtime has a hard block date in August 2027, and the Python 3.16 items have a year of warning left. As a result, the team can spread the work over several weeks without anything surprising them in between. The trade-off is that the base tsconfig change touches every package in one pull request, so it needs a quiet week rather than a Friday afternoon.
When to Use This Deprecated APIs Sweep
- Before bumping TypeScript, Python, Node.js or Next.js to the current major in a codebase older than a year
- When a build fails after a routine dependency update and you need the full list of removals, not only the first error
- Before the 2027-08-31 Lambda update block, to find every function definition still on a 2026-deprecated runtime
- When auditing an inherited repository, to see which upgrades it skipped
- As a pull request gate, so new code cannot reintroduce an API that is already gone
When NOT to Rely on the Sweep Alone
- The deprecated API sits in a dependency, which a text search of your repository will not see, so you need the runtime warning flags instead
- Calls are built dynamically, such as
getattr(glob, name)or computedrequirepaths, which no grep can match - You need behavior changes such as the UTF-8 default, which have no syntax to search for
- The stack is not covered here, such as Java, Go or Ruby, where each has its own deprecation tooling
- Infrastructure is defined in a console instead of in code, so runtime identifiers never appear in a file
Common Mistakes with Deprecated APIs
- Adding
"ignoreDeprecations": "6.0"to get through TypeScript 6 and never removing the options it silenced - Assuming no output means no deprecated APIs, when Python hides warnings from imported modules by default
- Testing only with warnings enabled, which misses imports such as
CGIHTTPRequestHandlerthat never warned - Treating a Lambda deprecation date as the deadline, when the real breakage is the later update block on a hotfix day
- Renaming edge-runtime
middleware.tstoproxy.ts, which moves it to the Node.js runtime - Fixing the deprecated call but keeping a
multipleResolveslistener, which now fails without a sound - Scoping the upgrade by package instead of by deadline, so already-broken items wait behind future ones
Keeping This Deprecated APIs Tracker Current
This page is a tracker for deprecated APIs, on a three-month review cycle plus a check on each new major of TypeScript, Python, Node.js or Next.js, with the next re-check on 2027-01-16. To refresh it, re-run node-deps.sh against the newest Node tag and compare the stage-change list. Next, run the TypeScript and Python probes on the newest release. Then re-read the Lambda table for runtimes whose dates moved into the next year. Python 3.16 and Next.js 17 are the next releases likely to add rows, since both already have removals scheduled.
Next Steps
Deprecated APIs in 2026 broke in two ways. TypeScript 7, Python 3.15 and Node.js 26 removed APIs outright and fail loudly. Lambda, GitHub Actions and Python’s own warning filters fail quietly, either on a date or inside imported code nobody runs with warnings on. The fastest way to find out where your code stands is to run the sweep from this post against your repository today. Then fix the hits in deadline order, starting with anything tagged ts7, gha or py3.15, since those are already broken on the current releases. If Node.js is the biggest slice of your list, the Node.js 24 breaking changes guide covers the earlier removals that the 26 upgrade builds on.