322

Is there a way in python to programmatically determine the width of the console? I mean the number of characters that fits in one line without wrapping, not the pixel width of the window.

Edit

Looking for a solution that works on Linux

anatoly techtonik
  • 19,847
  • 9
  • 124
  • 140
Sergey Golovchenko
  • 18,203
  • 15
  • 55
  • 72
  • Look this answer for a more extensive solution to have a "columns dependent" printing mechanism. https://stackoverflow.com/questions/44129613/how-to-print-number-of-characters-based-on-terminal-width-that-also-resize/44133299#44133299 – Riccardo Petraglia May 23 '17 at 11:29
  • 2
    Please consider changing the accepted answer. The one you've selected is really janky, platform dependent, and it's using `os.popen` which is deprecated. The top-voted answer showing `shutil` is the best way. – wim Dec 12 '20 at 06:37

15 Answers15

335

Not sure why it is in the module shutil, but it landed there in Python 3.3. See:

Querying the size of the output terminal

>>> import shutil
>>> shutil.get_terminal_size((80, 20))  # pass fallback
os.terminal_size(columns=87, lines=23)  # returns a named-tuple

A low-level implementation is in the os module. Cross-platform—works under Linux, Mac OS, and Windows, probably other Unix-likes. There's a backport as well, though no longer relevant.

Gringo Suave
  • 29,931
  • 6
  • 88
  • 75
  • 1
    Neat find, but it wasn't ever put into 2.7, it seems. – ArtOfWarfare Jun 24 '14 at 21:35
  • 36
    That's because you shouldn't be using 2.7 any longer, make the jump to 3.x it's worth it. – anthonyryan1 Jun 25 '15 at 02:29
  • 1
    This is simply the way it should be done. It even updates as the console width changes. +1 – dsclose Sep 03 '15 at 17:57
  • 6
    @osirisgothra Many hosting providers do not support python3 yet, so some of us are forced to use python2 for back end development. Though that should have nothing to do with getting terminal size... – whitebeard Mar 24 '16 at 19:32
  • 1
    I was going to dig this one out of the Python 3 Lib/ source like usual, but it actually relies on an exported c function called `os.terminal_size` sadly. The backport package is pure python & MIT licensed however and can just be thrown into your own package source. – Bryce Guinta Jul 12 '16 at 22:11
  • 4
    @osirisgothra Because there's a lot of Python 2 code that would take too much work to port. Also, Pypy still has rather poor Python 3 support. – Antimony Aug 27 '16 at 22:15
  • 2
    @whitebeard Is there a reason the subscriber can't install Python 3 on a VPS? Or in 2016, are people still using shared hosting whose administrator is unwilling to install Python 3? For example, WebFaction shared hosting has `python3.5` installed. – Damian Yerrick Nov 13 '16 at 13:06
  • 1
    @Damian Yerrick No, on a VPS where subscriber has control, he can install what he likes. Yes, there are still shared hosting services that do not have and will not install Python 3. Hopefully, that will change soon, but it is still a reality. – whitebeard Nov 15 '16 at 10:38
  • 3
    +1 for a solution that also works when standard input has been redirected from a file! With other solutions, I was either getting `Inappropriate ioctl for device` errors/warnings, or getting the defined fallback value of 80. – pawamoy Feb 15 '18 at 11:21
  • 2
    "Not sure why it is in the module shutil".. FYI `shutil` means `sh`ell `util`ities. Getting the terminal size is surely a utility function for dealing with the shell so it's *the* module you'd expect to have such functionality. Where else do you want to put that? `sys`? `os`? they are for more general stuff... – Bakuriu Dec 05 '18 at 19:07
  • @Bakuriu A shell and terminal are two different things. The basic implementation is in the os module. Perhaps in a "termutils" module, or the full impl in os would have been a good idea. Point is moot however, as decision was already made and not breaking now. – Gringo Suave Dec 07 '18 at 18:51
  • 1
    This doesn't appear to work when the python script is executed from a pipe in the terminal (it reverted to the 'fallback' case provided to `get_terminal_size()`). I'm now using `subprocess.check_output(['tput', 'cols'])` instead. – knowah Oct 28 '19 at 11:01
  • Cygwin compatible. Confirming. – Hans Deragon Mar 12 '20 at 13:55
280
import os
rows, columns = os.popen('stty size', 'r').read().split()

uses the 'stty size' command which according to a thread on the python mailing list is reasonably universal on linux. It opens the 'stty size' command as a file, 'reads' from it, and uses a simple string split to separate the coordinates.

Unlike the os.environ["COLUMNS"] value (which I can't access in spite of using bash as my standard shell) the data will also be up-to-date whereas I believe the os.environ["COLUMNS"] value would only be valid for the time of the launch of the python interpreter (suppose the user resized the window since then).

(See answer by @GringoSuave on how to do this on python 3.3+)

boxed
  • 3,895
  • 2
  • 24
  • 26
brokkr
  • 3,043
  • 2
  • 17
  • 7
  • 1
    You can get this to work on Solaris as well if instead of "size" you pass "-a". There will be "rows = Y; columns = X" in the semicolon delimited output. – Joseph Garvin Mar 11 '10 at 15:28
  • Testing on Ubuntu, it appears -a works on Linux as well but the output is formatted slightly differently -- there's no '=' between rows/columns and the number. – Joseph Garvin Mar 11 '10 at 15:30
  • 5
    COLUMNS isn't exported by default in Bash, that's why os.environ["COLUMNS"] doesn't work. – Kjell Andreassen Dec 05 '12 at 17:33
  • Order is wrong. It should be: `columns, rows = os.popen('stty size', 'r').read().split()` – Aleksandr Levchuk Sep 21 '14 at 02:41
  • `stty size` works under solaris 5.8 (no need to parse -a).You may want to use `rows, columns = [int(i) for i in os.popen('stty size', 'r').read().split()]` to get integers directly – yota Nov 26 '14 at 10:12
  • 42
    `rows, columns = subprocess.check_output(['stty', 'size']).split()` is a little shorter, plus subprocess is the future – cdosborn Mar 17 '15 at 02:00
  • 10
    tput is better than stty, as stty cannot work with PIPE. – liuyang1 Jul 14 '15 at 12:35
  • I used this for a long time, but it's actually not a very good solution. For starters if you want to put it in a loop (to continually check screen size for changes), you're going to have a bad time spawning all those processes. Furthermore, sometimes the popen() returns nothing, which will raise an error if you do something like "width, height = ...." I've now gone with pascal's answer, and it seems to work so far. – J.J Apr 20 '16 at 22:04
  • 6
    `rows, columns = subprocess.check_output(['stty', 'size']).decode().split()` If you want unicode strings for py2/3 compatibility – Bryce Guinta Aug 02 '16 at 19:55
  • If I run it under `nohup` it gives following error: `ValueError: not enough values to unpack (expected 2, got 0) @brokkr ` – alper Apr 05 '18 at 07:28
  • 3
    Do not use a process (especially from the deprecated os.popen), for this except as a last resort. – Gringo Suave Sep 06 '18 at 22:35
  • 1
    Cygwin compatible. Confirming. However, creating a new process is costly. I prefer to use the _shutil.get_terminal_size()_ solution of @GringoSuave – Hans Deragon Mar 12 '20 at 13:55
  • This generates following error: `stty: 'standard input': Inappropriate ioctl for device` – alper Jun 09 '20 at 16:42
  • This is a terrible answer that shows why "it's working" is not a high enough bar for anything. Use the right tool for the job, not your grandma's favourite bash trick. – Eugene Pankov May 08 '21 at 18:46
66

use

import console
(width, height) = console.getTerminalSize()

print "Your terminal's width is: %d" % width

EDIT: oh, I'm sorry. That's not a python standard lib one, here's the source of console.py (I don't know where it's from).

The module seems to work like that: It checks if termcap is available, when yes. It uses that; if no it checks whether the terminal supports a special ioctl call and that does not work, too, it checks for the environment variables some shells export for that. This will probably work on UNIX only.

def getTerminalSize():
    import os
    env = os.environ
    def ioctl_GWINSZ(fd):
        try:
            import fcntl, termios, struct, os
            cr = struct.unpack('hh', fcntl.ioctl(fd, termios.TIOCGWINSZ,
        '1234'))
        except:
            return
        return cr
    cr = ioctl_GWINSZ(0) or ioctl_GWINSZ(1) or ioctl_GWINSZ(2)
    if not cr:
        try:
            fd = os.open(os.ctermid(), os.O_RDONLY)
            cr = ioctl_GWINSZ(fd)
            os.close(fd)
        except:
            pass
    if not cr:
        cr = (env.get('LINES', 25), env.get('COLUMNS', 80))

        ### Use get(key[, default]) instead of a try/catch
        #try:
        #    cr = (env['LINES'], env['COLUMNS'])
        #except:
        #    cr = (25, 80)
    return int(cr[1]), int(cr[0])
chown
  • 51,908
  • 16
  • 134
  • 170
Johannes Weiss
  • 52,533
  • 16
  • 102
  • 136
  • 5
    Thanks for quick reply, but here (http://effbot.org/zone/console-handbook.htm) it says that "The Console module is currently only available for Windows 95, 98, NT, and 2000." I am looking for a solution that works on Linux. It probably wasn't clear from the tag, I will edit the question accordingly. – Sergey Golovchenko Feb 19 '09 at 19:22
  • 2
    since this "console" module you're using is not on the standard python library, you should provide its source code or at least a link to it. – nosklo Feb 19 '09 at 19:22
  • I'm so sorry about that. In fact, I didn't know that module. I tryied import console and it worked, I used console. and getTerminalSize() showed up. Instead of looking where it's from I already posted an answer because I was so lucky of the simplicity *g* – Johannes Weiss Feb 19 '09 at 19:29
  • I might be looking at a different "console" module, could you please provide a link for the one you have? – Sergey Golovchenko Feb 19 '09 at 19:31
  • umnik, the code I posted is the complete console "module". Just put the code in console.py and it will work as in my first sample – Johannes Weiss Feb 19 '09 at 19:33
  • Works both Windows and Linux, Thank you! – Sergey Golovchenko Feb 19 '09 at 19:42
  • Actually, I see on Windows it goes to the exception handler and returns the hardcoded (80,25). Anyways, I was looking for a solution that works on Linux, and it does quite well. – Sergey Golovchenko Feb 19 '09 at 19:53
  • And, this will not work if you `pipe` your Python program output to, say, `less`. You would get the exception: `IOError: [Errno 25] Inappropriate ioctl for device` – Sridhar Ratnakumar Sep 18 '09 at 21:29
  • .. to fix that: return 80 if `not sys.stdout.isatty()` – Sridhar Ratnakumar Sep 18 '09 at 21:37
  • Hey Johannes, if you incorporate the stty answer below and Sridhar's idea from these comments I'll upvote ;) – Joseph Garvin Mar 11 '10 at 15:29
  • I'd like to know where is this console.py from. I want a link to the project. – Denilson Sá Maia Apr 12 '11 at 16:46
  • This function's started to appear all over the interweb (making it near impossible to figure out where it first originated). Yet has no-one noticed that 'env' is not defined before the last try-except block? So it'll always fail, returning (25,80)... An `env=os.environ` line beforehand should fix that. Also, the os import is out of scope... – Alex Leach Jun 25 '12 at 16:24
  • Another one: os.environ contains strings, so that should be something like try: width = int(os.environ['COLUMNS']) / except (KeyError,ValueError): width = 80 – Paul Du Bois Feb 20 '13 at 05:10
  • 4
    Oh, and not to pile on the code, but "cr" is a confusing name because it implies the tuple is (cols, rows). In reality, it is the reverse. – Paul Du Bois Feb 20 '13 at 05:23
  • I wondered whether this snippet comes from https://github.com/nvie/rq/commit/aebfe74630da0e042038d958bdc1e870fbc28f76 Thanks for your answer – dylanninin Jun 22 '15 at 04:30
  • @dylanninin no, possibly the other way around though :). This answer is from 2009 and the code you linked is from 2011. The code is from the open-source code base of my former company. – Johannes Weiss Jun 22 '15 at 09:46
  • `module 'console' has no attribute 'getTerminalSize'` – alper Jun 09 '20 at 16:43
60

Code above didn't return correct result on my linux because winsize-struct has 4 unsigned shorts, not 2 signed shorts:

def terminal_size():
    import fcntl, termios, struct
    h, w, hp, wp = struct.unpack('HHHH',
        fcntl.ioctl(0, termios.TIOCGWINSZ,
        struct.pack('HHHH', 0, 0, 0, 0)))
    return w, h

hp and hp should contain pixel width and height, but don't.

pascal
  • 601
  • 5
  • 2
  • 4
    This is how it should be done; note that if you intend to print to the terminal, you should use '1' as the file descriptor (first argument of ioctl), as stdin might be a pipe or some different tty. – mic_e Mar 10 '13 at 20:41
  • 1
    Perhaps the 0 should be replaced with `fcntl.ioctl(sys.stdin.fileno(), ...` – raylu Jul 30 '13 at 21:55
  • 4
    this is the best answer - your users will be happy that there's not a surprise subprocess happening just to get term width – zzzeek Nov 20 '13 at 18:56
  • 4
    this is indeed the cleanest answer. I think you should use `stdout` or `stderr` instead of `stdin`, though. `stdin` might very well be a pipe. You might also want to add a line such as `if not os.isatty(0): return float("inf")`. – mic_e May 15 '14 at 23:51
  • this somehow works on a chromebook terminal which has like no functionality. +1 – hyper-neutrino Nov 15 '17 at 14:27
  • @pascal @raylu I have found, actually, the best luck is had by replacing the `0` with a call to `os.ctermid()`, which returns the name of the terminal device for the current process – like e.g. `/dev/tty` – fish2000 May 27 '20 at 06:53
  • I am getting following error: ` OSError: [Errno 25] Inappropriate ioctl for device ` – alper Jun 09 '20 at 16:48
50

It's either:

import os
columns, rows = os.get_terminal_size(0)
# or
import shutil
columns, rows = shutil.get_terminal_size()

The shutil function is just a wrapper around os one that catches some errors and set up a fallback, however it has one huge caveat - it breaks when piping!, which is a pretty huge deal.
To get terminal size when piping use os.get_terminal_size(0) instead.

First argument 0 is an argument indicating that stdin file descriptor should be used instead of default stdout. We want to use stdin because stdout detaches itself when it is being piped which in this case raises an error.

I've tried to figure out when would it makes sense to use stdout instead of stdin argument and have no idea why it's a default here.

jamesdlin
  • 81,374
  • 13
  • 159
  • 204
Granitosaurus
  • 20,530
  • 5
  • 57
  • 82
38

I searched around and found a solution for windows at :

http://code.activestate.com/recipes/440694-determine-size-of-console-window-on-windows/

and a solution for linux here.

So here is a version which works both on linux, os x and windows/cygwin :

""" getTerminalSize()
 - get width and height of console
 - works on linux,os x,windows,cygwin(windows)
"""

__all__=['getTerminalSize']


def getTerminalSize():
   import platform
   current_os = platform.system()
   tuple_xy=None
   if current_os == 'Windows':
       tuple_xy = _getTerminalSize_windows()
       if tuple_xy is None:
          tuple_xy = _getTerminalSize_tput()
          # needed for window's python in cygwin's xterm!
   if current_os == 'Linux' or current_os == 'Darwin' or  current_os.startswith('CYGWIN'):
       tuple_xy = _getTerminalSize_linux()
   if tuple_xy is None:
       print "default"
       tuple_xy = (80, 25)      # default value
   return tuple_xy

def _getTerminalSize_windows():
    res=None
    try:
        from ctypes import windll, create_string_buffer

        # stdin handle is -10
        # stdout handle is -11
        # stderr handle is -12

        h = windll.kernel32.GetStdHandle(-12)
        csbi = create_string_buffer(22)
        res = windll.kernel32.GetConsoleScreenBufferInfo(h, csbi)
    except:
        return None
    if res:
        import struct
        (bufx, bufy, curx, cury, wattr,
         left, top, right, bottom, maxx, maxy) = struct.unpack("hhhhHhhhhhh", csbi.raw)
        sizex = right - left + 1
        sizey = bottom - top + 1
        return sizex, sizey
    else:
        return None

def _getTerminalSize_tput():
    # get terminal width
    # src: http://stackoverflow.com/questions/263890/how-do-i-find-the-width-height-of-a-terminal-window
    try:
       import subprocess
       proc=subprocess.Popen(["tput", "cols"],stdin=subprocess.PIPE,stdout=subprocess.PIPE)
       output=proc.communicate(input=None)
       cols=int(output[0])
       proc=subprocess.Popen(["tput", "lines"],stdin=subprocess.PIPE,stdout=subprocess.PIPE)
       output=proc.communicate(input=None)
       rows=int(output[0])
       return (cols,rows)
    except:
       return None


def _getTerminalSize_linux():
    def ioctl_GWINSZ(fd):
        try:
            import fcntl, termios, struct, os
            cr = struct.unpack('hh', fcntl.ioctl(fd, termios.TIOCGWINSZ,'1234'))
        except:
            return None
        return cr
    cr = ioctl_GWINSZ(0) or ioctl_GWINSZ(1) or ioctl_GWINSZ(2)
    if not cr:
        try:
            fd = os.open(os.ctermid(), os.O_RDONLY)
            cr = ioctl_GWINSZ(fd)
            os.close(fd)
        except:
            pass
    if not cr:
        try:
            cr = (env['LINES'], env['COLUMNS'])
        except:
            return None
    return int(cr[1]), int(cr[0])

if __name__ == "__main__":
    sizex,sizey=getTerminalSize()
    print  'width =',sizex,'height =',sizey
Harco Kuppens
  • 389
  • 3
  • 3
  • You saved me the time of doing this myself. Works on Linux. Should work on Windows as well. Thanks! – Steve V. Sep 23 '11 at 20:20
23

Starting at Python 3.3 it is straight forward: https://docs.python.org/3/library/os.html#querying-the-size-of-a-terminal

>>> import os
>>> ts = os.get_terminal_size()
>>> ts.lines
24
>>> ts.columns
80
Bob Enohp
  • 1,302
  • 12
  • 11
6

It looks like there are some problems with that code, Johannes:

  • getTerminalSize needs to import os
  • what is env? looks like os.environ.

Also, why switch lines and cols before returning? If TIOCGWINSZ and stty both say lines then cols, I say leave it that way. This confused me for a good 10 minutes before I noticed the inconsistency.

Sridhar, I didn't get that error when I piped output. I'm pretty sure it's being caught properly in the try-except.

pascal, "HHHH" doesn't work on my machine, but "hh" does. I had trouble finding documentation for that function. It looks like it's platform dependent.

chochem, incorporated.

Here's my version:

def getTerminalSize():
    """
    returns (lines:int, cols:int)
    """
    import os, struct
    def ioctl_GWINSZ(fd):
        import fcntl, termios
        return struct.unpack("hh", fcntl.ioctl(fd, termios.TIOCGWINSZ, "1234"))
    # try stdin, stdout, stderr
    for fd in (0, 1, 2):
        try:
            return ioctl_GWINSZ(fd)
        except:
            pass
    # try os.ctermid()
    try:
        fd = os.open(os.ctermid(), os.O_RDONLY)
        try:
            return ioctl_GWINSZ(fd)
        finally:
            os.close(fd)
    except:
        pass
    # try `stty size`
    try:
        return tuple(int(x) for x in os.popen("stty size", "r").read().split())
    except:
        pass
    # try environment variables
    try:
        return tuple(int(os.getenv(var)) for var in ("LINES", "COLUMNS"))
    except:
        pass
    # i give up. return default.
    return (25, 80)
thejoshwolfe
  • 5,328
  • 3
  • 29
  • 21
  • I was wandering about the `env` too, and it is indeed `env = os.environ`, from [accepted answer](http://stackoverflow.com/a/566752/277826). – sdaau May 01 '13 at 00:02
6

Many of the Python 2 implementations here will fail if there is no controlling terminal when you call this script. You can check sys.stdout.isatty() to determine if this is in fact a terminal, but that will exclude a bunch of cases, so I believe the most pythonic way to figure out the terminal size is to use the builtin curses package.

import curses
w = curses.initscr()
height, width = w.getmaxyx()
wonton
  • 7,568
  • 9
  • 56
  • 93
2

Try "blessings"

I was looking for the very same thing. It is very easy to use and offers tools for coloring, styling and positioning in the terminal. What you need is as easy as:

from blessings import Terminal

t = Terminal()

w = t.width
h = t.height

Works like a charm in Linux. (I'm not sure about MacOSX and Windows)

Download and documentation here

or you can install it with pip:

pip install blessings
Iman Akbari
  • 2,167
  • 26
  • 31
  • Nowadays I'd say try "blessed" which is a currently maintained fork (and enhancement) of "blessings". – zezollo Jun 21 '20 at 20:47
1

I was trying the solution from here that calls out to stty size:

columns = int(subprocess.check_output(['stty', 'size']).split()[1])

However this failed for me because I was working on a script that expects redirected input on stdin, and stty would complain that "stdin isn't a terminal" in that case.

I was able to make it work like this:

with open('/dev/tty') as tty:
    height, width = subprocess.check_output(['stty', 'size'], stdin=tty).split()
Marc Liyanage
  • 4,601
  • 2
  • 28
  • 28
1

If you're using Python 3.3 or above, I'd recommend the built-in get_terminal_size() as already recommended. However if you are stuck with an older version and want a simple, cross-platform way of doing this, you could use asciimatics. This package supports versions of Python back to 2.7 and uses similar options to those suggested above to get the current terminal/console size.

Simply construct your Screen class and use the dimensions property to get the height and width. This has been proven to work on Linux, OSX and Windows.

Oh - and full disclosure here: I am the author, so please feel free to open a new issue if you have any problems getting this to work.

Peter Brittain
  • 13,489
  • 3
  • 41
  • 57
0

@reannual's answer works well, but there's an issue with it: os.popen is now deprecated. The subprocess module should be used instead, so here's a version of @reannual's code that uses subprocess and directly answers the question (by giving the column width directly as an int:

import subprocess

columns = int(subprocess.check_output(['stty', 'size']).split()[1])

Tested on OS X 10.9

rickcnagy
  • 1,774
  • 18
  • 24
0

Use subprocess, it is the most convenient way of doing it:

Import:

import subprocess

Example of Use:

print(subprocess.check_output(['stty', 'size']).split())

Note: This function returns bytes but you can cast it to Integer with the int() function.

Note: this function returns an array, being: array[0]=rows and array[1]=columns.

Output:

[b'46', b'188']

For instance if you need to compare whether the width of your console is larger than W, you can do something like this:

if int(subprocess.check_output(['stty', 'size']).split()[1]) > W:
    ...

A.Casanova
  • 555
  • 4
  • 16
-1

Here is an version that should be Linux and Solaris compatible. Based on the posts and commments from madchine. Requires the subprocess module.

def termsize():
    import shlex, subprocess, re
    output = subprocess.check_output(shlex.split('/bin/stty -a'))
    m = re.search('rows\D+(?P\d+); columns\D+(?P\d+);', output)
    if m:
        return m.group('rows'), m.group('columns')
    raise OSError('Bad response: %s' % (output))
>>> termsize()
('40', '100')
Community
  • 1
  • 1
Derrick Petzold
  • 1,118
  • 13
  • 13