Timer convenience class to time code execution for user output I implemented a convenience class to time code execution and print it to the user. This is part of a bigger project that will be distributed to external users. The timer however is only called by other procedures, not by the user directly. I was interested in implementing this myself, so I'm not looking for an external module or similiar. Docstrings are omitted here, the functions are rather straightforward.
from time import perf_counter


class Timer:
    def __init__(self, default_round_digits: int = 5):
        self.default_digits = default_round_digits
        self.start_timer()

    def start_timer(self) -&gt; None:
        self.start_time = perf_counter()
        self.pause_start_time = None
        self.pause_length = 0

    def pause_timer(self) -&gt; None:
        self.pause_start_time = perf_counter()

    def resume_timer(self) -&gt; None:
        if self.pause_start_time is not None:
            self.pause_length += perf_counter() - self.pause_start_time
            self.pause_start_time = None

    @property
    def elapsed_seconds(self) -&gt; float:
        # If timer is paused only consider time up to self.pause_start_time instead of now
        if self.pause_start_time is not None:
            return self.pause_start_time - self.start_time - self.pause_length
        else:
            return perf_counter() - self.start_time - self.pause_length

    def get_timer(self, round_digits: int = None, print_text: str = None, restart: bool = False) -&gt; float:

        if round_digits is None:
            round_digits = self.default_digits

        elapsed_seconds = round(self.elapsed_seconds, ndigits=round_digits)

        if print_text is not None:
            print(f&quot;{print_text}{elapsed_seconds} seconds&quot;)

        if restart:
            self.start_timer()

        return elapsed_seconds

    def __str__(self) -&gt; str:
        state = &quot;Running&quot; if self.pause_start_time is None else &quot;Paused&quot;
        return f&quot;{state} Timer at {self.get_timer()} seconds&quot;

I'm still unsure about initializing attributes outside of __init__. I know it's not recommended, but copying start_timer into __init__ seemed like the worse option to me.
Your Timer class contains two timers.

The entire duration timer.
The timer for tracking pauses.

I'd move the core timer code out into a separate class.
And we can see you're starting to repeat yourself.

Initializing a timer shouldn't start the timer.
If I open my timer app should the timer start running straight away?
If I want to create some timers but I don't want the timer to be started your class gives me no options.
However just not calling start_timer in __init__ will allow everyone to use your class however is desired.
If you really really want to start the timer when building the object, you can write a class method.
class Timer:
    @classmethod
    def run(cls, *args, **kwargs):
        self = cls(*args, **kwargs)
        self.start_timer()
        return self

# option 1
timer = Timer()
timer.start_timer()

# option 2
timer = Timer.run()


start_timer restarts the timers.
You should rename the method or make the method do what is expected.

If you call pause_timer twice without calling resume_timer your code acts as if the timer was never paused the first time.

I don't really see the point of making elapsed_seconds a property.

get_timer is doing way too much and violates the SRP principle.

Rounds elapsed timings to a specified amount of decimal places.
Prints the elapsed timings.
Restarts the timer.
Returns the elapsed timings.

You should make a function to round the elapsed timings.
Then leave printing and restarting to the user.

I'd a couple more methods from functionality I'd expect from a timer.


from time import perf_counter


class CoreTimer:
    def __init__(self):
        self.stop()

    def __bool__(self):
        return bool(self.start_time)

    def start(self):
        self.start_time = perf_counter()

    def stop(self):
        self.start_time = 0

    def time_taken(self, now):
        return now - self.start_time

    def elapsed(self):
        return self.time_taken(perf_counter())


class Timer:
    def __init__(self, decimal_places=5):
        self._decimal_places = decimal_places
        self._timer = CoreTimer()
        self._pause = CoreTimer()
        self._pause_length = 0

    def __str__(self):
        state = &quot;Paused&quot; if self._pause else &quot;Running&quot;
        return f&quot;{state} Timer at {self.elapsed_rounded()} seconds&quot;

    @classmethod
    def run(cls, *args, **kwargs):
        self = cls(*args, **kwargs)
        self.start()
        return self

    def start(self):
        if not self._timer:
            self._timer.start()

    def stop(self):
        self._timer.stop()
        self._pause.stop()
        self._pause_length = 0

    def restart(self):
        self.stop()
        self.start()

    def pause(self):
        if not self._pause:
            self._pause.start()

    def resume(self):
        if self._pause:
            self._pause_length += self._pause.elapsed()
            self._pause.stop()

    def elapsed(self):
        if self._pause:
            duration = self._timer.time_taken(self._pause.start_time)
        else:
            duration = self._timer.elapsed()
        return duration - self._pause_length

    def elapsed_rounded(self, decimal_places=None):
        if decimal_places is None:
            decimal_places = self.decimal_places
        return round(self.elapsed(), ndigits=decimal_places)