docs: def read_slice(self, firstrow, lastrow, step=1, **keys):
          """
          Read the specified row slice from a table.
  
          Read all rows between firstrow and lastrow (non-inclusive, as per
          python slice notation).  Note you must use slice notation for
          images, e.g. f[ext][20:30, 40:50]
  
          parameters
          ----------
          firstrow: integer
              The first row to read
          lastrow: integer
              The last row to read, non-inclusive.  This follows the python list
              slice convention that one does not include the last element.
          step: integer, optional
              Step between rows, default 1. e.g., if step is 2, skip every other
              row.
          vstorage: string, optional
              Over-ride the default method to store variable length columns.  Can
              be 'fixed' or 'object'.  See docs on fitsio.FITS for details.
          lower: bool, optional
              If True, force all columns names to lower case in output. Will over
              ride the lower= keyword from construction.
          upper: bool, optional
              If True, force all columns names to upper case in output. Will over
              ride the lower= keyword from construction.
          """
  
          if self._info['hdutype'] == ASCII_TBL:
              rows = numpy.arange(firstrow, lastrow, step, dtype='i8')
              keys['rows'] = rows
              return self.read_ascii(**keys)
  
          step = keys.get('step', 1)
          if self._info['hdutype'] == IMAGE_HDU:
              raise ValueError("slices currently only supported for tables")
  
          maxrow = self._info['nrows']
          if firstrow < 0 or lastrow > maxrow:
              raise ValueError(
                  "slice must specify a sub-range of [%d,%d]" % (0, maxrow))
  
          dtype, offsets, isvar = self.get_rec_dtype(**keys)
  
          w, = numpy.where(isvar == True)  # noqa
          if w.size > 0:
              vstorage = keys.get('vstorage', self._vstorage)
              rows = numpy.arange(firstrow, lastrow, step, dtype='i8')
              colnums = self._extract_colnums()
              array = self._read_rec_with_var(
                  colnums, rows, dtype, offsets, isvar, vstorage)
          else:
              if step != 1:
                  rows = numpy.arange(firstrow, lastrow, step, dtype='i8')
                  array = self.read(rows=rows)
              else:
                  # no +1 because lastrow is non-inclusive
                  nrows = lastrow - firstrow
                  array = numpy.zeros(nrows, dtype=dtype)
  
                  # only first needs to be +1.  This is becuase the c code is
                  # inclusive
                  self._FITS.read_as_rec(self._ext+1, firstrow+1, lastrow, array)
  
                  array = self._maybe_decode_fits_ascii_strings_to_unicode_py3(
                      array)
  
                  for colnum, name in enumerate(array.dtype.names):
                      self._rescale_and_convert_field_inplace(
                          array,
                          name,
                          self._info['colinfo'][colnum]['tscale'],
                          self._info['colinfo'][colnum]['tzero'])
  
          lower = keys.get('lower', False)
          upper = keys.get('upper', False)
          if self.lower or lower:
              _names_to_lower_if_recarray(array)
          elif self.upper or upper:
              _names_to_upper_if_recarray(array)
  
          self._maybe_trim_strings(array, **keys)
  
          return array