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
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.