Title: The new import system makes it inconvenient to correctly issue a deprecation warning for a module
Type: behavior Stage: resolved
Components: Library (Lib) Versions: Python 3.6, Python 3.5
Status: closed Resolution: fixed
Dependencies: Superseder:
Assigned To: brett.cannon Nosy List: Arfrever, barry, berker.peksag, brett.cannon, eric.snow, josh.r, jwilk, larry, ncoghlan, njs, python-dev, serhiy.storchaka, takluyver, zach.ware
Priority: release blocker Keywords: 3.2regression, patch

Created on 2015-05-27 23:36 by njs, last changed 2015-09-07 05:38 by python-dev. This issue is now closed.

File name Uploaded Description Edit
issue24305.diff brett.cannon, 2015-08-15 23:32 Skip internal CPython implementation frames review
issue24305.diff brett.cannon, 2015-08-18 01:05 review
issue24305.diff brett.cannon, 2015-08-19 02:22 Hybrid Nathaniel/Brett approach review
issue24305.diff brett.cannon, 2015-08-23 17:01 Subtly buggy C implementation review
issue24305.diff brett.cannon, 2015-08-28 23:10
issue24305.diff brett.cannon, 2015-08-29 00:20 Passes all tests, Python and C review
Messages (40)
msg244225 - (view) Author: Nathaniel Smith (njs) * (Python committer) Date: 2015-05-27 23:36
(Noticed while fixing the IPython equivalent of issue 24294)

The obvious way to deprecate a module is to issue a DeprecationWarning inside the main body of the module, i.e.

import warnings
warnings.warn("{} is deprecated".format(__name__), DeprecationWarning)

import thirdpartymodule

But this is problematic, because the resulting message will claim that the problem is in, not in And this is especially bad if I am doing things correctly (!) and using a warnings filter that enables display of DeprecationWarnings for mymodule, but not for third-party modules. (This need for correct attribution comes up in the interactive use case cited above, but I actually have packages where the CI infrastructure requires the elimination of DeprecationWarnings triggered by my own code -- for this to work it's crucial that warnings be attributed correctly.)

So the obvious fix is to instead write:

import warnings
warnings.warn("{} is deprecated".format(__name__), DeprecationWarning,

which says "the code that needs fixing is the code that called me".

On Python 2.7, this works, because all the code that executes in between 'import thirdpartymodule' and the call to 'warnings.warn' is C code, so it doesn't create any intermediate stack frames.

On more recent versions of Python, the import system itself is written in Python, so this doesn't work at all.

On Python 3.3, the correct way to deprecate a module is:

warnings.warn("this module is deprecated", DeprecationWarning,

and on Python 3.4, the correct way to deprecate a module is:

warnings.warn("this module is deprecated", DeprecationWarning,

(See for test code.)

Obviously this is not desireable.

I'm not sure what best solution is. Maybe there should be some collaboration between the import code and the warnings module, so that when the warnings module walks the stack, it skips over stack frames that come from inside the guts of import system?
msg244232 - (view) Author: Nick Coghlan (ncoghlan) * (Python committer) Date: 2015-05-28 00:38
Somewhat related issues are issue 16217 (traceback noise when running under python -m) and issue 23773 (traceback noise when running under custom import hooks)

For 3.4 and 3.5, we may want to consider a brute force solution where the warnings module just straight up ignores _frozen_importlib frames when counting stack levels.

For 3.6+, I've occasionally pondered various ideas for introducing the notion of a "frame debugging level". The most recent variant of that idea would be to have all frames live in level zero by default, but there'd be a way to flag a module at compile time to set it higher for the code objects created by that module.

Displaying those frames in tracebacks would then be tied to both the current frame's debugging level and the interpreter's verbosity level (whichever was higher), while other stack related operations (like the warnings stacklevel and frame retrieval) would only consider frames at the current frame's debugging level and below.
msg244247 - (view) Author: Berker Peksag (berker.peksag) * (Python committer) Date: 2015-05-28 03:04
This looks like a duplicate of issue 23810 (there is a patch for stdlib usages, but it probably can be changed to a more general solution).
msg244259 - (view) Author: Nick Coghlan (ncoghlan) * (Python committer) Date: 2015-05-28 04:02
I've made this depend on issue 23810, rather than duplicating it.

That way, issue 23810 can cover fixing the stdlib deprecation warnings, while this can cover making it a public API.
msg244302 - (view) Author: Brett Cannon (brett.cannon) * (Python committer) Date: 2015-05-28 14:37
My personal plan was to get issue #23810 finished, make sure it worked, and then expose a public API for declaring module deprecations which used the private API under the hood. I'm hoping to get #23810 done this Friday and then we can talk about how we may want to expose a function in the warnings module for deprecating modules.
msg244310 - (view) Author: Serhiy Storchaka (serhiy.storchaka) * (Python committer) Date: 2015-05-28 15:48
It would be better to skip _frozen_importlib frames automatically instead of forcing end users to use special API.
msg244390 - (view) Author: Brett Cannon (brett.cannon) * (Python committer) Date: 2015-05-29 16:54
Skipping select frames is a shift in semantics for warnings.warn() (so can't go into 3.5b1), doing it implicitly might be costly on interpreters where getting a frame is expensive, and coming up with a new API allows for a nicer design, e.g. `warnings.deprecate_module(__name__, 'it will be removed in Python 3.6')` -> "the formatter module is deprecated; it will be removed in Python 3.6"
msg244448 - (view) Author: Nathaniel Smith (njs) * (Python committer) Date: 2015-05-30 06:10
A nice new API is certainly a nice thing, but I feel that changing the stack walking code should be considered as fixing a regression introduced in 3.3. Indeed, searching github for code that actually uses the stacklevel= argument:

I observe that:

- there isn't a single usage on the first ten pages with stacklevel > 3, so stack walking speed is unlikely to be an issue -- esp. since it will only be slow in the cases where there are spurious import frames on the stack, i.e., you can only make it faster by making the results incorrect, and

- in the first ten pages I counted 14 distinct pieces of code (GH search is chock full of duplicates, sigh), and *11* of them are specifically module deprecations that are correctly passing stacklevel=2, and thus working on 2.7 but broken on 3.3+, and

- I counted zero examples where someone wrote

if sys.version_info[:2] == (3, 3):
    stacklevel = 10
elif sys.version_info[:2] == (3, 4):
    stacklevel = 8
    stacklevel = 2
warnings.warn("{} is deprecated".format(__name__), DeprecationWarning, stacklevel=stacklevel)

which is the only situation where even backporting a fix for this would break code that previously worked correctly.

Basically the overwhelming majority of uses of the stacklevel= argument are broken in 3.3 and 3.4 and 3.5-beta. We should fix it.
msg248602 - (view) Author: Brett Cannon (brett.cannon) * (Python committer) Date: 2015-08-14 18:30
I have merged over issue #23810 because they are aiming to solve the same problem and the conversation is split too much.

Just thinking out loud, this situation is compounded by the fact that importlib itself has some warnings and so automatically stripping out stack frames might make those warnings look odd.

Still not sure if Larry considers this a release blocker for 3.5.0. And any solution will need to be solved in and _warnings.c.

Some days I really hate our warnings system.
msg248603 - (view) Author: Serhiy Storchaka (serhiy.storchaka) * (Python committer) Date: 2015-08-14 19:24
Frames can be skipped only for warnings issued by imported module code, not for warnings issued by import machinery itself.

I propose following algorithm:

Before executing module code push the current frame and the number of frames to skip in global stack in the warnings module, and pop it after execution (public context manager can help). In warnings.warn() skip stacklevel frames and if saved frame was skipped, skip corresponding additional number of frames. Repeat for all saved frames.
msg248605 - (view) Author: Brett Cannon (brett.cannon) * (Python committer) Date: 2015-08-14 19:31
Will that be thread-safe? Plus storing that value in a way that _warnings and warnings can read will require something like a single-item list that can be mutated in-place.

The other option is to teach warnings to skip anything coming from importlib/_bootstrap* unless stacklevel==1 and that frame happens to be in the importlib code.
msg248609 - (view) Author: Nathaniel Smith (njs) * (Python committer) Date: 2015-08-14 19:40
For 3.4/3.5 purposes, I propose a simpler algorithm: first, define a function which takes a module name and returns true if it is part of the internal warning machinery and false otherwise. This is easy because we know what import machinery we ship.

Then, to walk the stack in, do something like:

frame = sys._get frame(1)
if is_import_machinery(frame.module_name):
  skip_frame = lambda modname: False
  skip_frame = is_import_machinery
def next_unskipped_frame(f):
  new = f
  while new is f or skip_frame(new.module_name):
    new = new.caller
for i in range(stacklevel - 1):
  frame = next_unskipped_frame(frame)

This produces reasonable behavior for warnings issued by both regular user code and by warnings issued from inside the warning machinery, and it avoids having to explicitly keep track of call depths.

Then we can worry about coming up with an all-singing all-dancing generalized version for 3.6.
msg248617 - (view) Author: Brett Cannon (brett.cannon) * (Python committer) Date: 2015-08-14 20:43
Nathaniel's solution is basically what I came up with in issue #23810 except I simply skipped subtracting from the stack level if it was found to be an internal import frame instead of swapping in and out a callable.
msg248635 - (view) Author: Nathaniel Smith (njs) * (Python committer) Date: 2015-08-15 08:32
Yeah, yours is probably better in fact, I was just trying to make the semantics as obvious as explicit as possible for a comment :-)
msg248670 - (view) Author: Brett Cannon (brett.cannon) * (Python committer) Date: 2015-08-15 23:32
Here is a pure Python patch that skips any frames that mentions 'importlib' and '_bootstrap' in the filename (it also skips _warnings.warn for easy testing since I didn't implement the C part). The only side-effect is that negative stack levels won't be the same because of the change in line numbers, but in that instance I'm not going to worry about it. It also causes test_venv and test_warnings to fail but I have not looked into why.

And still need a call by Larry as to whether this will go into 3.5.0.
msg248750 - (view) Author: Brett Cannon (brett.cannon) * (Python committer) Date: 2015-08-18 01:05
Here is a new version of the patch with Nathaniel's and and Berker's comments addressed.
msg248814 - (view) Author: Brett Cannon (brett.cannon) * (Python committer) Date: 2015-08-19 02:22
Here is an approach that somewhat merges Nathaniel's proposed solution and mine.
msg248918 - (view) Author: Brett Cannon (brett.cannon) * (Python committer) Date: 2015-08-20 21:53
Could someone do a quick code review of my latest patch? If the design looks sane I might be able to steal some time tomorrow to start on the C implementation.
msg248937 - (view) Author: Eric Snow (eric.snow) * (Python committer) Date: 2015-08-21 00:58
"Hybrid Nathaniel/Brett approach" LGTM
msg249011 - (view) Author: Brett Cannon (brett.cannon) * (Python committer) Date: 2015-08-23 17:01
Here is a patch that adds the C version of the frame skipping. Unfortunately it fails under test_threading, test_subprocess, test_multiprocessing_spawn. It's due to is_internal_frame() somehow although setting a breakpoint in gdb in that function never triggers. The return value is -11 from the interpreter and I don't remember what that represents.
msg249088 - (view) Author: Larry Hastings (larry) * (Python committer) Date: 2015-08-25 00:23
The C implementation is making me nervous.  My gut feeling is the Python implementation would be easier to get right.

I still don't quite understand: what is the user-perceived result of this change?  Module authors issuing a DeprecationWarning can now use stacklevel=2 instead of stacklevel=10?
msg249089 - (view) Author: Larry Hastings (larry) * (Python committer) Date: 2015-08-25 00:32
Is it really *impossible* to "correctly issue a deprecation warning for a module", as the title asserts?  Or does the new import system simply make it *tiresome*?

if sys.version_info.major == 3 and sys.version_info.minor == 4:
  stacklevel = 8
elif  sys.version_info.major == 3 and sys.version_info.minor == 4:
  stacklevel = 10
  stacklevel = 2 # I bet they fixed it in 3.6!

warnings.warn("{} is deprecated".format(__name__), DeprecationWarning,

That's Python for you, doing six "impossible" things before breakfast.
msg249090 - (view) Author: Nathaniel Smith (njs) * (Python committer) Date: 2015-08-25 00:36
> I still don't quite understand: what is the user-perceived result of this change?  Module authors issuing a DeprecationWarning can now use stacklevel=2 instead of stacklevel=10?

Exactly. There are a lot of deprecated modules in the wild, and the correct way to mark a module as deprecated is by writing

warnings.warn("This module is deprecated!", stacklevel=2)

at the top-level of your module. Except that in Python 3.3 you have to use stacklevel=10, and in Python 3.4 you have to use stacklevel=8, and I haven't checked what you need in Python 3.5 but it may well be different again, because it depends on internal implementation details of the import system. Fortunately AFAICT from some code searches no-one is actually depending on this though; instead everyone just writes stacklevel=2 and gets the wrong result.

(This is made more urgent b/c people are increasingly relying on the stacklevel information to filter whether they even see the DeprecationWarning. E.g. I've seen multiple test suites which register a warnings filter that displays DeprecationWarnings iff they are attributed to the package-under-test, and IPython now shows deprecation warnings by default if the warning is attributed to interactive code -- see issue 24294. So there's a good chance that users will only find out that the module they are using is deprecated if the stacklevel= is set correctly.)
msg249091 - (view) Author: Larry Hastings (larry) * (Python committer) Date: 2015-08-25 00:39
If this has been broken since 3.3, I don't think it's a release blocker for 3.5.  I'm willing to consider it a "bug" and accept a fix, but I'd prefer it to be as low-risk as possible (aka the Python version).  Can someone fix the regressions?

And, if the C fix is better somehow, let's definitely get that into 3.6.
msg249092 - (view) Author: Nathaniel Smith (njs) * (Python committer) Date: 2015-08-25 00:40
You're right, "impossible" is a slight exaggeration :-). As an alternative, every package could indeed carry around a table containing the details of importlib's call stack in every version of Python.

(I also haven't checked whether it's consistent within a single stable release series. I guess we could add a rule that 3.5.x -> 3.5.(x+1) cannot change the number of function calls inside the importlib callstack, because that is part of the public API, but I have less than perfect confidence that this rule has been enforced strictly in the past, and I don't think I'd want to be the one in charge of enforcing it in the future.)
msg249151 - (view) Author: Brett Cannon (brett.cannon) * (Python committer) Date: 2015-08-25 18:52
So there are two approaches I see to solving this whole thing. One is for there to be a slight divergence between the C code and the Python code. For _warnings.warn(), nothing changes. For warnings.warn(), though, it does the expected frame skipping. This would mean that _warnings.warn() continues to exist for startup purposes but then user-level code and non-startup code uses the Python version that has the expected semantics. This requires figuring out why not importing _warnings.warn in causes test failures.

The other option is someone helps me figure out why the C code is causing its test failures by triggering a -11 return in threaded/subprocess tests and prevent the divergence.

I have opened issue #24938 to actually re-evaluate one of the key points of _warnings.c which was startup performance. This was done about 7 years ago which pre-dates freezing code and providing simple C wrappers as necessary for internal use in Python like we do with importlib.
msg249306 - (view) Author: Brett Cannon (brett.cannon) * (Python committer) Date: 2015-08-29 00:20
I figured  out what was causing the threading/subprocess problems (something in frame->f_code->co-filename was NULL), so the attached patch covers Python, C, and adds a test (as well as cleaning up the test_warnings file structure since it was old-school spread out in Lib/test).

I don't think Larry wants this going into 3.5 based on his previous comments (period, not just 3.5.0). Is that true, Larry? If so, would you accept a 3.5.0 patch to fix the warnings in formatter and imp to a more accurate stacklevel?
msg249622 - (view) Author: Larry Hastings (larry) * (Python committer) Date: 2015-09-03 08:53
Well, this is making me nervous to apply during the RCs.  But... I'm willing to risk it.

My price: I want to see this run on a bunch of otherwise-healthy buildbots to make sure it doesn't break any platforms.

In case you've never done such a thing, here's how: create a "server-side clone" on, apply the patch to that tree, and push to the server-side clone.  Then grab a fistful of URLs to "custom" buildbots from here:

and visit each individual page, e.g.

and use the "Force Build" GUI at the bottom to kick off a build using the server-side clone with the patch applied.

Nathaniel: If Brett doesn't have the time to deal with this, I can make the server-side clone and check in the patch onto it.  You can then kick off the builds and report back the results.
msg249666 - (view) Author: Zachary Ware (zach.ware) * (Python committer) Date: 2015-09-03 15:53
Just to note, there's an easier way to run a custom build on multiple bots: go to and scroll (way) down to the section for forcing a build on custom builders (you can search for 'Repo path:' (with the colon)), check the box next to all the builders you want, then fill out the Repo path, your name, reason, and revision.  You can also force the build on *all* custom builders; search for the second hit on 'Repo path:'.

...might not hurt if we document that somewhere.
msg249687 - (view) Author: Larry Hastings (larry) * (Python committer) Date: 2015-09-03 20:28
That *is* easier, thanks.  Though the UI for that is baffling.  Protip: search for the section where all the "custom" builders are listed all in one section, three-quarters of the way down the page.
msg249907 - (view) Author: Larry Hastings (larry) * (Python committer) Date: 2015-09-05 12:22
Okay.  Right now creating server-side clones is broken.  So I have repurposed one of my existing (old, dead) server-side clones for testing this.  It's called "ssh://".

I just fired off this change on all the "custom" buildbots.  I'm going to bed.  If it passed everything in the morning I'll just check it in to 3.5.0, and hail mary.
msg249978 - (view) Author: Larry Hastings (larry) * (Python committer) Date: 2015-09-06 07:51
It was basically okay on the buildbots--no worse than cpython would have been before the checkin.  (A lot of the buildbots are... wonky.)

I checked it in to cpython350 directly.  I'll do the forward merge after 3.5.0rc3 goes out the door.
msg249980 - (view) Author: Nathaniel Smith (njs) * (Python committer) Date: 2015-09-06 07:59
Hooray! Thanks Larry.

Would it make sense to do a 3.4.x backport, or is that closed now with 3.5 being imminent?
msg249981 - (view) Author: Larry Hastings (larry) * (Python committer) Date: 2015-09-06 08:04
I don't think it'd be appropriate to backport to 3.4--that ship has sailed.  3.4 requires a stacklevel=8 and that's that.

If we backported it and it shipped in 3.4.4, "correct" code would have to use a stacklevel=8 for 3.4.0 through 3.4.3, and stacklevel=2 for 3.4.4 and above.  Ugh!
msg249984 - (view) Author: Nathaniel Smith (njs) * (Python committer) Date: 2015-09-06 08:56
Some limited code search statistics posted upthread (msg244448) suggest that ~100% of real-world code is using stacklevel=2 unconditionally and thus getting incorrect results on 3.3 and 3.4.
msg249985 - (view) Author: Larry Hastings (larry) * (Python committer) Date: 2015-09-06 08:57
Yes, I saw that.  That doesn't mean we should change the interface they are using (incorrectly) eighteen months after it shipped.  We take backwards-compatibility pretty seriously here in the Python world, bugs and all.
msg249986 - (view) Author: Nathaniel Smith (njs) * (Python committer) Date: 2015-09-06 09:04
Also just checked, which allows more fine-grained queries, and it reports ~6500 hits for "stacklevel=2" and exactly 0 for "stacklevel=8".

<refreshes, sees new post>

Huh. So the official word is that requiring stacklevel=8 on 3.4 is not a bug, rather all those modules are buggy? I've been telling people who asked to just leave the stacklevel=2 thing alone rather than adding version-specific conditionals, but I guess I can pass that on.
msg249987 - (view) Author: Larry Hastings (larry) * (Python committer) Date: 2015-09-06 09:05
Unless I'm overruled, yes.
msg249988 - (view) Author: Nathaniel Smith (njs) * (Python committer) Date: 2015-09-06 09:26
msg250069 - (view) Author: Roundup Robot (python-dev) (Python triager) Date: 2015-09-07 05:38
New changeset 94966dfd3bd3 by Larry Hastings in branch '3.5':
Issue #24305: Prevent import subsystem stack frames from being counted
Date User Action Args
2015-09-07 05:38:03python-devsetnosy: + python-dev
messages: + msg250069
2015-09-06 09:26:03njssetmessages: + msg249988
2015-09-06 09:05:17larrysetmessages: + msg249987
2015-09-06 09:04:16njssetmessages: + msg249986
2015-09-06 08:57:47larrysetmessages: + msg249985
2015-09-06 08:56:26njssetmessages: + msg249984
2015-09-06 08:04:19larrysetmessages: + msg249981
2015-09-06 07:59:10njssetmessages: + msg249980
2015-09-06 07:51:11larrysetstatus: open -> closed
resolution: fixed
messages: + msg249978

stage: commit review -> resolved
2015-09-05 12:22:30larrysetmessages: + msg249907
2015-09-03 20:28:03larrysetmessages: + msg249687
2015-09-03 15:53:34zach.waresetnosy: + zach.ware
messages: + msg249666
2015-09-03 15:51:58brett.cannonsetassignee: larry -> brett.cannon
2015-09-03 08:53:37larrysetmessages: + msg249622
2015-08-29 00:20:53brett.cannonsetfiles: + issue24305.diff
priority: critical -> release blocker
messages: + msg249306

assignee: brett.cannon -> larry
stage: patch review -> commit review
2015-08-28 23:10:44brett.cannonsetfiles: + issue24305.diff
2015-08-28 19:01:57brett.cannonsetpriority: deferred blocker -> critical
assignee: larry -> brett.cannon
2015-08-25 18:52:51brett.cannonsetmessages: + msg249151
2015-08-25 00:43:23larrysettitle: The new import system makes it impossible to correctly issue a deprecation warning for a module -> The new import system makes it inconvenient to correctly issue a deprecation warning for a module
2015-08-25 00:40:43njssetmessages: + msg249092
2015-08-25 00:39:55larrysetpriority: release blocker -> deferred blocker

messages: + msg249091
2015-08-25 00:36:20njssetmessages: + msg249090
2015-08-25 00:32:07larrysetmessages: + msg249089
2015-08-25 00:23:37larrysetmessages: + msg249088
2015-08-23 17:01:35brett.cannonsetfiles: + issue24305.diff

messages: + msg249011
2015-08-22 02:45:44josh.rsetnosy: + josh.r
2015-08-21 00:58:52eric.snowsetmessages: + msg248937
2015-08-20 21:53:43brett.cannonsetmessages: + msg248918
2015-08-19 02:22:16brett.cannonsetfiles: + issue24305.diff

messages: + msg248814
2015-08-18 01:05:11brett.cannonsetfiles: + issue24305.diff

messages: + msg248750
2015-08-15 23:32:09brett.cannonsetfiles: + issue24305.diff
messages: + msg248670

components: + Library (Lib)
keywords: + patch
stage: patch review
2015-08-15 08:32:47njssetmessages: + msg248635
2015-08-14 20:43:32brett.cannonsetmessages: + msg248617
2015-08-14 19:40:41njssetmessages: + msg248609
2015-08-14 19:31:26brett.cannonsetmessages: + msg248605
2015-08-14 19:24:31serhiy.storchakasetmessages: + msg248603
2015-08-14 18:30:59brett.cannonsetassignee: larry

nosy: + larry
2015-08-14 18:30:49brett.cannonsetpriority: normal -> release blocker
versions: + Python 3.5, Python 3.6
messages: + msg248602

dependencies: - Suboptimal stacklevel of deprecation warnings for formatter and imp modules
keywords: + 3.2regression
2015-08-14 18:18:57brett.cannonlinkissue23810 superseder
2015-06-07 12:56:01jwilksetnosy: + jwilk
2015-05-30 06:10:31njssetmessages: + msg244448
2015-05-29 16:54:13brett.cannonsetmessages: + msg244390
2015-05-28 15:48:57serhiy.storchakasetnosy: + serhiy.storchaka
messages: + msg244310
2015-05-28 14:37:28brett.cannonsetmessages: + msg244302
2015-05-28 13:39:59Arfreversetnosy: + Arfrever
2015-05-28 04:02:49ncoghlansetdependencies: + Suboptimal stacklevel of deprecation warnings for formatter and imp modules
messages: + msg244259
2015-05-28 03:04:54berker.peksagsetnosy: + berker.peksag
messages: + msg244247
2015-05-28 02:53:03barrysetnosy: + barry
2015-05-28 00:38:46ncoghlansetmessages: + msg244232
2015-05-28 00:05:14ned.deilysetnosy: + brett.cannon, ncoghlan, eric.snow
2015-05-27 23:47:39takluyversetnosy: + takluyver
2015-05-27 23:36:49njscreate