Suppose that stryng is a string which we wish to truncate and that nchars is the number of characters desired in the output string.
stryng = "sadddddddddddddddddddddddddddddddddddddddddddddddddd"
nchars = 10
We can truncate the string as follows:
def truncate(stryng:str, nchars:int):
    return (stryng[:nchars - 6] + " [...]")[:min(len(stryng), nchars)]
The results for certain test cases are shown below:
s = "sadddddddddddddddddddddddddddddd!"
s = "sa" + 30*"d" + "!"
truncate(s, 2)                ==  sa
truncate(s, 4)                ==  sadd
truncate(s, 10)               ==  sadd [...]
truncate(s, len(s)//2)        ==  sadddddddd [...]
My solution produces reasonable results for the test cases above.
However, some pathological cases are shown below:
Some Pathological Cases!
truncate(s, len(s) - 3)()       ==  sadddddddddddddddddddddd [...]
truncate(s, len(s) - 2)()       ==  saddddddddddddddddddddddd [...]
truncate(s, len(s) - 1)()       ==  sadddddddddddddddddddddddd [...]
truncate(s, len(s) + 0)()       ==  saddddddddddddddddddddddddd [...]
truncate(s, len(s) + 1)()       ==  sadddddddddddddddddddddddddd [...
truncate(s, len(s) + 2)()       ==  saddddddddddddddddddddddddddd [..
truncate(s, len(s) + 3)()       ==  sadddddddddddddddddddddddddddd [.
truncate(s, len(s) + 4)()       ==  saddddddddddddddddddddddddddddd [
truncate(s, len(s) + 5)()       ==  sadddddddddddddddddddddddddddddd 
truncate(s, len(s) + 6)()       ==  sadddddddddddddddddddddddddddddd!
truncate(s, len(s) + 7)()       ==  sadddddddddddddddddddddddddddddd!
truncate(s, 9999)()             ==  sadddddddddddddddddddddddddddddd!
Notably,
- When the string contains new-line characters (\n) there could be an issue.
- When nchars > len(s)we should print stringswithout trying to print the "[...]"
Below is some more code:
import io
class truncate:
    """
        Example of Code Which Uses truncate:
        ```
            s = "\r<class\n 'builtin_function_or_method'>"
            s = truncate(s, 10)()
            print(s)
                    ```
                Examples of Inputs and Outputs:
                        truncate(s, 2)()   ==  \r
                        truncate(s, 4)()   ==  \r<c
                        truncate(s, 10)()  ==  \r<c [...]
                        truncate(s, 20)()  ==  \r<class\n 'bu [...]
                        truncate(s, 999)() ==  \r<class\n 'builtin_function_or_method'>
                    ```
                Other Notes:
                    Returns a modified copy of string input
                    Does not modify the original string
            """
    def __init__(self, x_stryng: str, x_nchars: int) -> str:
        """
        This initializer mostly exists to sanitize function inputs
        """
        try:
            stryng = repr("".join(str(ch) for ch in x_stryng))[1:-1]
            nchars = int(str(x_nchars))
        except BaseException as exc:
            invalid_stryng =  str(x_stryng)
            invalid_stryng_truncated = repr(type(self)(invalid_stryng, 20)())
            invalid_x_nchars = str(x_nchars)
            invalid_x_nchars_truncated = repr(type(self)(invalid_x_nchars, 20)())
            strm = io.StringIO()
            print("Invalid Function Inputs", file=strm)
            print(type(self).__name__, "(",
                  invalid_stryng_truncated,
                  ", ",
                  invalid_x_nchars_truncated, ")", sep="", file=strm)
            msg = strm.getvalue()
            raise ValueError(msg) from None
        self._stryng = stryng
        self._nchars = nchars
    def __call__(self) -> str:
        stryng = self._stryng
        nchars = self._nchars
        return (stryng[:nchars - 6] + " [...]")[:min(len(stryng), nchars)]