Ability to modify python types by simple class declaration

Currently, if you define a class like this:

class str(str):
    def titlecase(self):
        return [word[0].upper() + word[1:] for word in self]

It works on objects that you call the str method on, but it doesn’t work on regular string literals. I suggest that when you do something like this :

class str(builtin=True):
    # Stuff goes here

it then applies to all strings, even string literals.

That was just an example, it should be implemented for all types.

1 Like

What for? What is the idea behind this feature? Do you have real cases where this would be useful? Thanks!

Ruby allows this, and it is considered a major wart in the language. Search for “Ruby monkeypatching”.

1 Like

“Because Ruby does it” is not a great reason to add something to Python.

What code could you write using this feature that isn’t possible today?

Are you replying to me? I said it was a wart, i.e. it should not be added to Python.

@jeanas Apologies! I read your comment as: it’s a python wart for not having this feature.

3 Likes

I think this is a terrible idea. Though for science i gave it a try:

import builtins
class str(str):
    def title2(self):
        return 'lolcats'


def hook():
    builtins.str = str

Running hook in a terminal blows up like so

C:\Users\csm10495\Desktop>python -i test.py
>>> hook()
>>> <press enter>
Readline internal error
Traceback (most recent call last):
  File "C:\Python3\Lib\site-packages\pyreadline3\console\console.py", line 807, in hook_wrapper_23
    raise TypeError('readline must return a string.')
TypeError: readline must return a string.
Exception ignored on calling ctypes callback function: <function hook_wrapper_23 at 0x000002BE3D4EF240>
Traceback (most recent call last):
  File "C:\Python3\Lib\site-packages\pyreadline3\console\console.py", line 821, in hook_wrapper_23
    _strncpy(cast(p, c_char_p), res, n + 1)
ctypes.ArgumentError: argument 2: <class 'TypeError'>: wrong type

Things are unhappy that a str is not a str. I’m pleasantly surprised it doesn’t work.

“works on” isn’t meaningful language to describe the behaviour.

str, by default, is a built-in type, not a “method”. Calling it - like calling any other type, such as the user-defined ones created with the class statement - creates a new instance of the type (this behaviour can be altered using __new__). The code that you show doesn’t modify the built-in type; it creates a new type and reuses the name for that new type. So of course existing objects of the type aren’t affected, and of course subsequent literals aren’t affected - when they’re created, their type is known directly, and is not looked up by name.

Syntax like this makes no sense because the purpose of the class statement fundamentally is to create new types, not to modify existing ones.

Aside from that, anything that tries to make literals use a different type from their usual built-in one is a non-starter, because objects get created and “frozen” as part of compiling the code for functions.

What you seem to actually want is to be able to add methods to the built-in types. That is called “monkey-patching”, as noted:

Actual monkey-patching looks fundamentally different. It is supported already in Python for user-defined types:

class Example:
    pass

x = Example()
try:
    x.method()
except AttributeError:
    print('the method does not exist.')

# Recall, the name "self" is only a convention. Written outside the class,
# that name isn't so meaningful.
def method(instance):
    print('calling the method on', instance)

# monkey-patch the method.
Example.method = method
# Now it can be looked up from an instance, whether created prior:
x.method()
# or later:
Example().method()

However, it is not supported for built-in types, for good reason. Aside from causing untold potential for confusion (suppose I import your library and now suddenly 1 + 1 literally doesn’t give 2 any more! And how am I meant to undo the effect?), it would require forgoing a huge suite of optimizations that take advantage of the fact that these built-in types are always exactly what the documentation says they are. In particular, methods on builtin types don’t need to be looked up by the usual attribute-lookup mechanisms; behind-the-scenes code can directly call the appropriate implementation function.

2 Likes

@kknechtel

suppose I import your library and now suddenly 1 + 1 literally doesn’t give 2 any more! And how am I meant to undo the effect?

You could have a module variable module.modified_builtins that you would have to set to True for those changes to take effect.

This wouldn’t work, since the objects (eg the str type object) are the same everywhere. You can’t have the changes take effect only within a module.

Fine, I guess it’s a no. But I have a similar suggestion, which I will post soon.

I think this is asking for Swift style extension methods?
https://docs.swift.org/swift-book/documentation/the-swift-programming-language/extensions/

It’s actually already possible to “curse” builtin types like str:

>>> def mymethod(self):
...     return self + " ook ook"
... 
>>> def monkeypatch(type_, func):
...     gc.get_referents(type_.__dict__)[0][func.__name__] = func
... 
>>> import gc
>>> monkeypatch(str, mymethod)
>>> "hello".mymethod()
'hello ook ook'

Curse with an existing function:

>>> monkeypatch(str, textwrap.dedent)
>>> """
...     hello
...     world
... """.dedent()
'\nhello\nworld\n'

This is not a recommendation. It’s hackish, that’s an abuse of reference counting, and it probably only works in CPython. :see_no_evil:

If anyone asks, you didn’t hear it from me…

2 Likes

Wow. Bravo!

Actually, you could have a module with the builtins unmodified, and import that last if you don’t want the builtin changes.