The important line is this one, within the to_local() method:
self = self - self.tz_offset
Instead of changing self (this worldtime object) so that it now represents the local time, you are actually setting it to be a completely new object, specifically, the result of self - self.tz_offset.
So why isn't the result of that a worldtime object?
Note that the types of object in this calculation are worldtime - timedelta. At the moment you haven't done anything to specify how to perform subtraction on your worldtime class, so worldtime automatically inherits its subtraction behavior from its parent class (datetime). But this means it gets treated like an ordinary datetime object (after all, it is really a datetime, just with a couple of extra attributes and methods).
So Python carries out a datetime - timedelta calculation, and the result is a datetime object, which it then assigns to self. Which is why your worldtime object seems to be 'changing' into a datetime.
How can we make it work?
There are two options:
1) Update our object instead of creating a new one
If we know our offset will always just be some hours, we could do something like:
def to_local(self):
    if self.UTC is True:
        self.hour = self.hour + self.tz_offset.hours
        self.UTC = False
BUT this WON'T work because (contrary to what I initially expected!):
- tz_offsetdoesn't have a- hoursattribute (when you create a- timedeltait stores the time as days, seconds and microseconds)
- datetimeobjects don't let you set the- hourdirectly like this
We could try changing the _hour attribute (which is how datetime stores its time internally), but changing 'private' attributes like this is generally a bad idea. Plus, we still have to turn tz_offset back into hours to do that calculation, and what happens if we later want to have an offset with hours and minutes? And we need to make sure our offset isn't taking us across a date boundary... (and probably other issues we haven't thought of!)
Better to let datetime do what it's good at, so:
2a) Let datetime handle the subtraction, but turn the result back into a worldtime
def to_local(self):
    if self.UTC is True:
        new_time = self - self.tz_offset
        self = worldtime(
            new_time.year,
            new_time.month,
            new_time.day,
            new_time.hour,
            new_time.minute,
            new_time.second,
        )
        self.UTC = False
Alternatively, as you mentioned, you could define the __sub__() special method to define what the - operator does on our worldtime objects.
2b) Override the - operator with __sub__()
Let's leave to_local() as
def to_local(self):
    if self.UTC is True:
        self = self - self.tz_offset
        self.UTC = False
But change how that - behaves. Here, we're basically moving what we did in 2a into a separate method called __sub__() (as in subtraction). This means that when Python hits the -, it passes the left and right operands into the __sub__() special method as self and other (respectively), and then returns the result of the method.
    def __sub__(self, other):
    new_time = self - other
    return worldtime(
        new_time.year,
        new_time.month,
        new_time.day,
        new_time.hour,
        new_time.minute,
        new_time.second,
    )
BUT when we run this, we get an error like this:
RecursionError: maximum recursion depth exceeded
What happened?
When Python hits the self - self.tz_offset in to_local(), it calls __sub__(self, self.tz_offset). So far, so good. But when it gets to self - other within __sub__(), we're still doing subtraction on a worldtime object, so Python dutifully calls __sub__(self, other) again...and again, and again, and gets stuck in an infinite loop!
We don't want that. Instead, once we're in __sub__() we just want to do normal datetime subtraction. So it should look like this:
    def __sub__(self, other):
    new_time = super().__sub__(other)
    return worldtime(
        new_time.year,
        new_time.month,
        new_time.day,
        new_time.hour,
        new_time.minute,
        new_time.second,
    )
Here, super().__sub__(other) means we're using the __sub__() method on the parent class instead. Here, that's datetime, so we get a datetime object back, and can create a new worldtime object from that.
The whole thing (with your print statements) now looks like this:
from datetime import datetime, timedelta
class worldtime(datetime):
    UTC = True
    tz_offset = timedelta(hours = -4)
    def __new__(cls, *args, **kwargs):
        #kwargs['tzinfo'] = dateutil.tz.tzutc()
        return super().__new__(cls, *args, **kwargs)
    def is_UTC(self):
        return self.UTC
    def to_local(self):
        print(f"type(self): {type(self)}")
        if self.UTC is True:
            self = self - self.tz_offset
            print(f"type(self): {type(self)}")
            print(self)
            self.UTC = False
    def __sub__(self, other):
        new_time = super().__sub__(other)
        return worldtime(
            new_time.year,
            new_time.month,
            new_time.day,
            new_time.hour,
            new_time.minute,
            new_time.second,
        )
dt = worldtime(2019, 8, 26, 12, 0, 0)
print (f"dt = {dt}   is_UTC(): {dt.is_UTC()}")
print (f"type(dt): {type(dt)}")
print (f"dir(dt): {dir(dt)}")
dt.to_local()
(I changed to 4-space tabs, as is standard in Python)
BUT...Is this the best way to do this?
Hopefully that's answered your questions about subclassing in Python.
But reflecting on the problem, I'm not sure if this is the best way to go. Subclassing built-ins can be complicated and easy to get wrong, datetimes themselves are already complicated and easy to get wrong. Subclassing datetime makes less sense as it's not straightforward to change them after creation, and creating a new object and setting it to self doesn't feel very neat.
I wonder if it would be better to use composition instead of inheritance. So worldtime would store a datetime object internally, and you can operate on that, and use the timezone support in the datetime module to manage your timezone conversion, and maybe just do it on-the-fly for returning the local time.
Something like:
from datetime import datetime, timedelta, timezone
class WorldTime:
    OFFSET = timedelta(hours=-4)
    # assumes input time is in UTC, not local time
    def __init__(self, year, month=None, day=None, hour=0, minute=0, second=0,
                 microsecond=0, tzinfo=timezone.utc, *, fold=0):
        self.dt_in_utc = datetime(year, month, day, hour, minute, second,
                                  microsecond, tzinfo, fold=fold)
    # convert to our timezone, and then make naive ("local time")
    def to_local(self):
        return self.dt_in_utc.astimezone(timezone(self.OFFSET)).replace(tzinfo=None)
dt = WorldTime(2019, 8, 26, 12, 0, 0)
print(dt.to_local())
# Gives:
# 2019-08-26 08:00:00
I've made it so that to_local() returns a datetime object, which you can then print out, or do whatever you want to with afterwards.
Edit
I had another experiment with inheriting from datetime, and I think the following should work:
from datetime import datetime, timedelta, timezone
class WorldTime(datetime):
    OFFSET = timedelta(hours=-4)
    def __new__(cls, *args, tzinfo=timezone.utc, **kwargs):
        return super().__new__(cls, *args, tzinfo=tzinfo, **kwargs)
    def __add__(self, other):
        result = super().__add__(other)
        return WorldTime(*result.timetuple()[:6], tzinfo=result.tzinfo,
                          fold=result.fold)
    def __sub__(self, other):
        "Subtract two datetimes, or a datetime and a timedelta."
        if not isinstance(other, datetime):
            if isinstance(other, timedelta):
                return self + -other
            return NotImplemented
        return super().__sub__(other)
    def to_local(self):
        return self.astimezone(timezone(self.OFFSET)).replace(tzinfo=None)
dt = WorldTime(2019, 8, 26, 12, 0, 0)
print(dt)
print(dt.to_local())  # local time
print(dt + timedelta(days=20, hours=7))  # 20 days, 7 hours in the future
print(dt - timedelta(days=40, hours=16))  # 40 days, 16 hours in the past
print(dt - WorldTime(2018, 12, 25, 15, 0, 0))  # time since 3pm last Christmas Day
# Output:
# 2019-08-26 12:00:00+00:00  # WorldTime
# 2019-08-26 08:00:00  # datetime
# 2019-09-15 19:00:00+00:00  # WorldTime
# 2019-07-16 20:00:00+00:00  # WorldTime
# 243 days, 21:00:00  # timedelta
So it looks like addition and subtraction of timedeltas returns a WorldTime object, and we can find the difference between two WorldTime objects as a timedelta.
This isn't rigorously tested, however, so proceed with caution!