Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

threading.Timer.__init__() should use immutable argument defaults for args and kwargs #61637

Closed
denversc mannequin opened this issue Mar 16, 2013 · 9 comments
Closed
Labels
stdlib Python modules in the Lib dir type-bug An unexpected behavior, bug, or error

Comments

@denversc
Copy link
Mannequin

denversc mannequin commented Mar 16, 2013

BPO 17435
Nosy @gvanrossum, @loewis, @terryjreedy, @bitdancer
Files
  • ThreadingTimerInitDefaultArgsIssueDemo.01.patch: C:\dev\cpython\ThreadingTimerInitDefaultArgsIssueDemo.01.patch
  • Note: these values reflect the state of the issue at the time it was migrated and might not reflect the current state.

    Show more details

    GitHub fields:

    assignee = None
    closed_at = <Date 2013-03-30.21:25:55.759>
    created_at = <Date 2013-03-16.00:17:20.719>
    labels = ['type-bug', 'library']
    title = 'threading.Timer.__init__() should use immutable argument defaults for args and kwargs'
    updated_at = <Date 2013-03-30.21:25:55.757>
    user = 'https://bugs.python.org/denversc'

    bugs.python.org fields:

    activity = <Date 2013-03-30.21:25:55.757>
    actor = 'r.david.murray'
    assignee = 'none'
    closed = True
    closed_date = <Date 2013-03-30.21:25:55.759>
    closer = 'r.david.murray'
    components = ['Library (Lib)']
    creation = <Date 2013-03-16.00:17:20.719>
    creator = 'denversc'
    dependencies = []
    files = ['29422']
    hgrepos = []
    issue_num = 17435
    keywords = ['patch']
    message_count = 9.0
    messages = ['184278', '184282', '184335', '184340', '184762', '185400', '185401', '185591', '185592']
    nosy_count = 6.0
    nosy_names = ['gvanrossum', 'loewis', 'terry.reedy', 'r.david.murray', 'denversc', 'python-dev']
    pr_nums = []
    priority = 'normal'
    resolution = 'fixed'
    stage = 'resolved'
    status = 'closed'
    superseder = None
    type = 'behavior'
    url = 'https://bugs.python.org/issue17435'
    versions = ['Python 3.3', 'Python 3.4']

    @denversc
    Copy link
    Mannequin Author

    denversc mannequin commented Mar 16, 2013

    The __init__() method of threading.Timer uses *mutable* default values for the "args" and "kwargs" arguments. Since the default argument objects are created once and re-used for each instance, this means that changing the args list or kwargs dict of a Timer object that used the argument defaults will specify those arguments to all future Timer objects that use the defaults too.

    def __init__(self, interval, function, args=[], kwargs={}):

    A fully backwards-compatible way to fix this is to instead use None as the default value for args and kwargs and just create a new list and/or dict inside __init__() if they are None. That way each new instance of Timer will get its very own args list and kwargs dict object.

    def __init__(self, interval, function, args=None, kwargs=None):
        ...
        self.args = args if args is not None else []
        self.kwargs = kwargs if kwargs is not None else {}

    Here is a sample script that reproduces the issue:

        import threading
    
        event = threading.Event()
        def func(*args, **kwargs):
            print("args={!r} kwargs={!r}".format(args, kwargs))
            event.set()
    
        timer1 = threading.Timer(1, func)
        timer1.args.append("blah")
        timer1.kwargs["foo"] = "bar"
    
        timer2 = threading.Timer(1, func)
        timer2.start()
        event.wait()

    Here is the example output when run before the fix:

    c:\dev\cpython>PCbuild\python_d.exe ThreadingTimerInitDefaultArgsIssueDemo.py
    args=('blah',) kwargs={'foo': 'bar'}
    [44758 refs, 17198 blocks]

    And after the fix:

    c:\dev\cpython>PCbuild\python_d.exe ThreadingTimerInitDefaultArgsIssueDemo.py
    args=() kwargs={}
    [47189 refs, 18460 blocks]

    As you can see, in the version without the fix, the elements added to timer1's args and kwargs were also given to timer2, which is almost certainly not what a user would expect.

    A proposed patch, ThreadingTimerInitDefaultArgsIssueDemo.01.patch, is attached. This fixes the issue, updates the docs, and adds a unit test.

    @denversc denversc mannequin added stdlib Python modules in the Lib dir type-bug An unexpected behavior, bug, or error labels Mar 16, 2013
    @bitdancer
    Copy link
    Member

    Hmm. This wasn't an issue before 3.3 because previously one couldn't subclass Timer. So yeah, this needs to be fixed, but only in 3.3 and tip.

    Thanks for the patch.

    @denversc
    Copy link
    Mannequin Author

    denversc mannequin commented Mar 16, 2013

    Thanks r.david.murray for your feedback. Although I disagree with your conclusion that this does not affect 2.7. Just try running the "sample script that reproduces the issue" from my first post and you will see the erroneous behaviour in 2.7. Even though threading.Timer is a function in 2.7 (instead of a class), it still ultimately returns a class whose args and kwargs members can be modified.

    @bitdancer
    Copy link
    Member

    I'm sorry, you are correct. I replied too quickly without thinking it through.

    @denversc
    Copy link
    Mannequin Author

    denversc mannequin commented Mar 20, 2013

    Thanks r.david.murray. I appreciate you taking the time to look at this issue!

    @terryjreedy
    Copy link
    Member

    The reported behavior is not a bug by our usual standards. The code is exactly as documented.
    manual: class threading.Timer(interval, function, args=[], kwargs={})
    docstring: t = Timer(30.0, f, args=[], kwargs={})

    Threading is not a beginner module. Any competent Python programmer who reads either of the above, or the code line you quoted, would expect exactly the behavior you report. I think we should presume that people who monkey-patch the class, which is an unusual thing to do, know what they are doing. The patch would break any such intentional usage. If the signature were to be changed, there should be a deprecation period first, preferably with a DeprecationWarning for mutation either through an instance or through .__init__.

    I do not see anything special about this particular function. If we change this use of [] and {} as defaults, then we should look at all such uses in the stdlib -- after pydev discussion. But I currently think we should leave well enough alone.

    The Timer class was added in Sept. 2001, rev 19727, issue bpo-428326. The patch was written by Itamar Shtull-Trauring, approved by Guido, and reviewed and committed by Martin.

    @gvanrossum
    Copy link
    Member

    I agree with the OP -- it's a simple fix and the current code definitely violates our recommendations. I see no reason not to submit this (if there's nothing *else* wrong with it -- it actually seems pretty complete). Not sure how important it is to fix in 2.7, but I don't see a problem with it either.

    @python-dev
    Copy link
    Mannequin

    python-dev mannequin commented Mar 30, 2013

    New changeset 2698920eadcd by R David Murray in branch '3.3':
    Issue bpo-17435: Don't use mutable default values in Timer.
    http://hg.python.org/cpython/rev/2698920eadcd

    New changeset 8c15e57830dd by R David Murray in branch 'default':
    Merge bpo-17435: Don't use mutable default values in Timer.
    http://hg.python.org/cpython/rev/8c15e57830dd

    @bitdancer
    Copy link
    Member

    Thanks, Denver.

    I'm choosing not to backport it to 2.7, but that can be done later if someone finds it worth doing.

    @ezio-melotti ezio-melotti transferred this issue from another repository Apr 10, 2022
    Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
    Labels
    stdlib Python modules in the Lib dir type-bug An unexpected behavior, bug, or error
    Projects
    None yet
    Development

    No branches or pull requests

    3 participants