Q:

Python Chess Game

I'm developing chess in python, and I've decided to make everything text-based before creating a GUI because it'd be my first time doing the latter. I haven't got around to the actual gameplay, but I've modeled the possible movement of any piece. 
Note that I cannot implement En Passant and castling without turn data.
from time import sleep

class Config:
    DEMTIME = .8  # seconds
    types = {'min': 1, 'miniature': 3, 'small': 5, 'default': 8, 'extended': 11, 'large': 15, 'massive': 20, 'max': 26}
    letters = tuple('abcdefghijklmnopqrstuvwxyz')
    white_pieces = {'Pawn' : "♙", 'Rook' : "♖", 'Knight' : "♘", 'Bishop' : "♗", 'King' : "♔", 'Queen' : "♕" }
    black_pieces = {'Pawn' : "♟", 'Rook' : "♜", 'Knight' : "♞", 'Bishop' : "♝", 'King' : "♚", 'Queen' : "♛" }
    board = 'UNINITIALIZED'
    b_len = 'UNINITIALIZED'

    @classmethod
    def new_board(cls, btype='default'):
        def size(x):
            return [['___' for _ in range(x)] for _ in range(x)], x

        s = False

        if btype is not None:
            btype = btype.lower()

            if 'custom' in btype:
                btype = int(btype.replace('custom', '').strip())
                if 1 <= btype <= 26:
                    cls.board, cls.b_len = size(btype)
                    s = True
                else:
                    btype = None
                    cls.new_board(btype)
            elif btype in cls.types:
                cls.board, cls.b_len = size(cls.types[btype])
                s = True
            else:
                print(f'Unable to initialize board of unknown type {btype}')

        else:
            print('Unable to initalize board with a size lower than 1 or greater than 26')

        if s: cls.print_board()

    @classmethod
    def print_board(cls):
        if Config.b_len != 'UNINITIALIZED':
            def printl():
                if len(str(cls.b_len)) == 2:
                    print(' ', end='')
                for x in range(cls.b_len):
                    print(' '*6 + f'{cls.letters[x]}', end='')
                print('\n')

            print('\n'*2)
            printl()
            for x in range(cls.b_len):
                print(f'{cls.b_len-x:0{len(str(cls.b_len))}}  {cls.board[x]}  {cls.b_len-x:0{len(str(cls.b_len))}}\n')
            printl()
            print('\n'*4)

        else:
            print('Unable to print board of uninitialized type')

    @classmethod
    def tile_convert(cls, x, display_tile=False):
        if not display_tile:
            if isinstance(x, str):
                return cls.letters.index(x)
            else:
                return cls.letters[x]
        else:  # display_tile converts the letter in {letter}{number} to a number
            return cls.b_len - int(x)

    @classmethod
    def l_num_to_coord(cls, pos):
        return Config.b_len - int(pos[1]), int(Config.tile_convert(pos[0]))

    @classmethod
    def coord_to_tile(cls, x, y):
        return f'{Config.tile_convert(x)}{Config.tile_convert(y, True)}'

    @classmethod
    def c_convert(cls, color):
        if color == 'White':
            return 'b'
        if color == "Black":
            return 'w'

class ChessPiece:
    def __init__(self, pos, color, num, piece):
        self.x = int(Config.tile_convert(pos[0]))
        self.y = Config.b_len - int(pos[1])
        self.color = color
        self.piece = piece
        self.pieceid = num
        self.moves = 0
        self.captured = []
        self.erased = False
        self.set_id()
        self.create()
        Config.print_board()

    def __str__(self):
        return self.piece

    def __repr__(self):
        return self.pieceid

    def set_id(self):
        if self.__class__.__name__ != "Knight":
            self.pieceid = f'{self.piece[0]}{self.pieceid}'
        else:
            self.pieceid = f'N{self.pieceid}'

        if self.color is not None:
            if self.color.lower() in ('black', 'white', 'b', 'w'):
                self.pieceid = self.color.lower()[0] + self.pieceid
                if self.color.lower() == 'b':
                    self.color = 'Black'
                elif self.color.lower() == 'w':
                    self.color = 'White'
            else:
                self.color = None
                print("Invalid color input. Color not set.")
                self.set_id()
        else:
             self.pieceid = '_' + self.pieceid

    def create(self):
        if Config.board[self.y][self.x] != '___':
            po = Config.board[self.y][self.x]
            print(f'Piece {po} erased to make room for {self.pieceid}')

        Config.board[self.y][self.x] = self.pieceid

    def teleport(self, pos, record=False):
        Config.board[self.y][self.x] = '___'

        if record:
            self.moves += 1
            coord = Config.l_num_to_coord(pos)
            if Config.board[coord[0]][coord[1]] != '___':
                self.captured.append(Config.board[coord[0]][coord[1]])
                print(f'{self.pieceid} has captured {Config.board[coord[0]][coord[1]]}!')

        self.x = Config.tile_convert(pos[0])
        self.y = Config.tile_convert(pos[1], True)

        Config.board[self.y][self.x] = self.pieceid

        Config.print_board()

    def move(self, pos):
        if pos in self.possible_moves():
            coord = Config.l_num_to_coord(pos)
            if Config.board[coord[0]][coord[1]] != '___':
                self.captured.append(Config.board[coord[0]][coord[1]])
                print(f'{self.pieceid} has captured {Config.board[coord[0]][coord[1]]}!')
                # Erase piece

            if self.__class__ == Pawn:
                if abs(int(pos[1]) - Config.tile_convert(self.y, True)) == 2:
                    self.two_move = True

            self.teleport(pos)
            self.moves += 1

        else:
            print(f'Unable to move to {pos}')

    def get_info(self):
        print(f'{self.__class__.__name__}:\n')
        print('ID: ', self.pieceid)
        print('Position: ', Config.tile_convert(self.x), Config.tile_convert(self.y, True), sep='')
        print('Color: ', self.color)

    def erase(self):  # Doesn't delete the piece. It can be brought back by moving it to a square
        Config.board[self.y][self.x] = '___'
        self.erased = True

    def demo(self, rec=True):  # default board
        for pos in self.demo_moves:
            self.teleport(pos, rec)
            sleep(Config.DEMTIME)

        if self.__class__ == Pawn:
            self.promote2(Queen)

    @staticmethod
    def castle(king, rook):
        if not king.moves and not rook.moves:
            if not king.in_check:
                pass

class Pawn(ChessPiece):
    def __init__(self, pos='a1', color=None, num='_'):
        ChessPiece.__init__(self, pos, color, num, self.__class__.__name__)
        self.demo_moves = ('e1', 'e2', 'e3', 'e4', 'e5', 'e6', 'e7', 'e8')
        self.two_move = False

    def possible_moves(self):
        pos_moves = []

        x, y = self.x, self.y

        # Forward
        if not self.moves: # Starting Position
            if self.color != 'White':
                for new_y in (y+1, y+2):
                    try:
                        if Config.board[new_y][x] == '___':
                            pos_moves.append(Config.coord_to_tile(x, new_y))
                        else: break
                    except IndexError: pass

            if self.color != 'Black':
                for new_y in (y-1, y-2):
                    try:
                        if Config.board[new_y][x] == '___':
                            pos_moves.append(Config.coord_to_tile(x, new_y))
                        else: break
                    except IndexError: pass

        else:  # Post-Start
            if self.color != 'White':
                try:
                    if Config.board[y+1][x] == '___':
                        pos_moves.append(Config.coord_to_tile(x, y+1))
                except IndexError: pass

            if self.color != 'Black':
                try:
                    if Config.board[y-1][x] == '___':
                        pos_moves.append(Config.coord_to_tile(x, y-1))
                except IndexError: pass

        # Capturing
        if self.color != 'White':
            if self.color is not None:
                try:
                    if Config.c_convert(self.color) in Config.board[y+1][x+1]:
                        pos_moves.append(Config.coord_to_tile(x+1, y+1))
                except IndexError: pass
            else:
                try:
                    if Config.board[y+1][x+1] != '___':
                        pos_moves.append(Config.coord_to_tile(x+1, y+1))
                except IndexError: pass

        if self.color != 'Black':
            if self.color is not None:
                try:
                    if Config.c_convert(self.color) in Config.board[y-1][x-1]:
                        pos_moves.append(Config.coord_to_tile(x-1, y-1))
                except IndexError: pass
            else:
                try:
                    if Config.board[y+1][x+1] != '___':
                        pos_moves.append(Config.coord_to_tile(x-1, y-1))
                except IndexError: pass

        # En Passant

        return sorted(pos_moves)

    def promote(self, piece):  # oringal_piece = original_piece.promote(new_piece)
        pos = f'{Config.tile_convert(self.x)}{Config.tile_convert(self.y, True)}'

        return piece(pos, color=self.color, num='p')

    def promote2(self, piece):
        pos = f'{Config.tile_convert(self.x)}{Config.tile_convert(self.y, True)}'

        self.__class__ = piece
        self.__init__(pos, self.color, 'p')

class Knight(ChessPiece):
    def __init__(self, pos='a1', color=None, num='_'):
        ChessPiece.__init__(self, pos, color, num, self.__class__.__name__)
        self.demo_moves = ('e1', 'f3', 'g5', 'h7', 'f8', 'e6', 'c5', 'd3', 'e1')

    def possible_moves(self):
        pos_moves = []
        for x_off, y_off in ( (1, 2), (-1, 2), (1, -2), (-1, -2), (2, 1), (-2, 1), (2, -1), (-2, -1) ):
            new_x = self.x + x_off
            new_y = self.y + y_off
            if 0 <= new_x < Config.b_len and 0 <= new_y < Config.b_len:
                if self.color is not None:
                    if self.color[0].lower() not in Config.board[new_y][new_x]:
                        pos_moves.append(Config.coord_to_tile(new_x, new_y))
                else:
                    pos_moves.append(Config.coord_to_tile(new_x, new_y))

        return sorted(pos_moves)

class Bishop(ChessPiece):
    def __init__(self, pos='a1', color=None, num='_'):
        ChessPiece.__init__(self, pos, color, num, self.__class__.__name__)
        self.demo_moves = ('a1', 'e5', 'b8', 'h2', 'e5', 'a1')

    def possible_moves(self):
        pos_moves = []

        x, y = self.x, self.y

        right_up = zip(range(x + 1, Config.b_len), range(y - 1, -1, -1))
        right_down = zip(range(x + 1, Config.b_len), range(y + 1, Config.b_len))

        left_up = zip(range(x - 1, -1, -1), range(y - 1, -1, -1))
        left_down = zip(range(x - 1, -1, -1), range(y + 1, Config.b_len))

        for r in (right_up, right_down, left_up, left_down):
            for new_x, new_y in r:
                if self.color is not None:
                    if self.color[0].lower() not in Config.board[new_y][new_x]:
                        pos_moves.append(Config.coord_to_tile(new_x, new_y))
                        if Config.board[new_y][new_x] != '___': break
                    else: break
                else:
                    pos_moves.append(Config.coord_to_tile(new_x, new_y))

        return sorted(pos_moves)

class Rook(ChessPiece):
    def __init__(self, pos='a1', color=None, num='_'):
        ChessPiece.__init__(self, pos, color, num, self.__class__.__name__)
        self.demo_moves = ('a1', 'a8', 'h8', 'h1', 'a1')

    def possible_moves(self):
        pos_moves = []

        x, y = self.x, self.y

        # Horizontal
        for r in (range(x+1, Config.b_len), reversed(range(x))):
            for new_x in r:
                if self.color is not None:
                    if self.color[0].lower() not in Config.board[y][new_x]:
                        pos_moves.append(Config.coord_to_tile(new_x, y))
                        if Config.board[y][new_x] != '___': break
                    else: break
                else:
                    pos_moves.append(Config.coord_to_tile(new_x, y))
                    if Config.board[new_y][new_x] != '___': break

        # Vertical
        for r in (range(y+1, Config.b_len), reversed(range(y))):
            for new_y in r:
                if self.color is not None:
                    if self.color[0].lower() not in Config.board[new_y][x]:
                        pos_moves.append(Config.coord_to_tile(x, new_y))
                        if Config.board[new_y][x] != '___': break
                    else: break
                else:
                    pos_moves.append(Config.coord_to_tile(x, new_y))
                    if Config.board[new_y][new_x] != '___': break

        return sorted(pos_moves)

class Queen(ChessPiece):
    def __init__(self, pos='a1', color=None, num='_'):
        ChessPiece.__init__(self, pos, color, num, self.__class__.__name__)
        self.demo_moves = ('a1', 'h8', 'a8', 'h1', 'a1')

    def possible_moves(self):
        pos_moves = []

        x, y = self.x, self.y

        # Horizontal
        for r in (range(x+1, Config.b_len), reversed(range(x))):
            for new_x in r:
                if self.color is not None:
                    if self.color[0].lower() not in Config.board[y][new_x]:
                        pos_moves.append(Config.coord_to_tile(new_x, y))
                        if Config.board[y][new_x] != '___': break
                    else: break
                else:
                    pos_moves.append(f'{Config.tile_convert(new_x)}{Config.tile_convert(y, True)}')
                    if Config.board[new_y][new_x] != '___': break

        # Vertical
        for r in (range(y+1, Config.b_len), reversed(range(y))):
            for new_y in r:
                if self.color is not None:
                    if self.color[0].lower() not in Config.board[new_y][x]:
                        pos_moves.append(Config.coord_to_tile(x, new_y))
                        if Config.board[new_y][x] != '___': break
                    else: break
                else:
                    pos_moves.append(f'{Config.tile_convert(x)}{Config.tile_convert(new_y, True)}')
                    if Config.board[new_y][new_x] != '___': break

        #Diagonal
        right_up = zip(range(x + 1, Config.b_len), range(y - 1, -1, -1))
        right_down = zip(range(x + 1, Config.b_len), range(y + 1, Config.b_len))

        left_up = zip(range(x - 1, -1, -1), range(y - 1, -1, -1))
        left_down = zip(range(x - 1, -1, -1), range(y + 1, Config.b_len))

        for r in (right_up, right_down, left_up, left_down):
            for new_x, new_y in r:
                if self.color is not None:
                    if self.color[0].lower() not in Config.board[new_y][new_x]:
                        pos_moves.append(Config.coord_to_tile(new_x, new_y))
                        if Config.board[new_y][new_x] != '___': break
                    else: break
                else:
                    pos_moves.append(Config.coord_to_tile(new_x, new_y))

        return sorted(pos_moves)

class King(ChessPiece):
    def __init__(self, pos='a1', color=None, num='_'):
        ChessPiece.__init__(self, pos, color, num, self.__class__.__name__)
        self.demo_moves = ('e4', 'd5', 'c4', 'c5', 'd6', 'e5', 'e4')
        self.in_check = False

    def possible_moves(self):
        pos_moves = []

        x, y = self.x, self.y

        for x_off, y_off in ( (0, 1), (0, -1), (1, 0), (-1, 0), (1, 1), (-1, 1), (-1, -1), (1, -1) ):
            new_x = self.x + x_off
            new_y = self.y + y_off
            if 0 <= new_x < Config.b_len and 0 <= new_y < Config.b_len:
                if self.color is not None:
                    if self.color[0].lower() not in Config.board[new_y][new_x]:
                        pos_moves.append(Config.coord_to_tile(new_x, new_y))
                else:
                    pos_moves.append(Config.coord_to_tile(new_x, new_y))

        return sorted(pos_moves)

# Some commands to start out with

Config.new_board('default')

r1 = Rook(color='w')
n1 = Knight('a5', color='b')
p1 = Pawn('e1', color='w')
p2 = Pawn('e8', color='b')
p3 = Pawn('f7', color='w')
r1.teleport('b3')

I plan to use the IDs in order to dictate piece images on the GUI.

A:

Style
The overall style is good and mostly follows the official style guide. Since you have a lot of code and the scope will likely expand even more, I would highly recommend using an automatic style checking tool like pylint, flake8, or the like. There are a lot of Python IDEs which integrate nicely with those style checkers (Visual Studio Code, PyCharm, Spyder, ...) and can even annotate (some also autofix) your code while you write. This will help you to keep a consistent code style even on a larger scope.
The main thing I would critique about the code's style is the sometimes inconsistent usage of vertical whitespace. The usual conventions is to have at most one line of vertical whitespace within functions, methods, and classes. Individual functions and classes should be seperated by two blank lines. There are also usually two blank lines following the imports.
Overall structure
The overall structure is quite good. Nevertheless I would actually recommend splitting the file into three files. A file for the chess pieces, one for the config/board and an application script which ties everything together. In order to pull this off, the classes might need a little bit of work, but we will come to that later.
The application script should also likely make use of the infamous if __name__ == "__main__":, to make it clear which parts are actually supposed to be run as script.
The classes
Config
This class is actually the piece of code I like the least. First, I don't like the name. It is generic and IMHO does not really fit what the class is about. While looking through the code, it becomes more and more apparent that Config is the actual chessboard. So why not call it just like that, Chessboard?
The second thing I don't like about this class is the way that classmethods are used. I can see why it might look appealing to define everything as attributes and methods of the class in order to avoid having to pass around the board to the chesspieces. But I think there are better alternatives to do this.
Some parts of it can stay as class attributes such as types, white_pieces, and black_pieces1, although I would capitalize their names in order to make it clear that these are supposed to be seen as constants. Since you are also using letters in a few places, it might be also be legetimate to have them as class attribute, though I would at least prepend the name with an _ to make it clear that this is an implementation detail and not supposed to be used by an ordinary user. 
So what could the new class look like?
class Chessboard:
    """The chessboard to play on

    The board can have different sizes, the default one has 8x8 fields. There
    is a list of predefined sizes in Chessboard.TYPES, and you can request a
    custom size by passing "custom<n>", where <n> is should be convertible to
    an integer between 1 and 26, as an argument to the constructor.
    """

    TYPES = {
        'min': 1, 'miniature': 3, 'small': 5, 'default': 8,
        'extended': 11, 'large': 15, 'massive': 20, 'max': 26
    }

    WHITE_PIECES = {
        'Pawn': "♙", 'Rook': "♖", 'Knight': "♘",
        'Bishop': "♗", 'King': "♔", 'Queen': "♕"
    }

    BLACK_PIECES = {
        'Pawn': "♟", 'Rook': "♜", 'Knight': "♞", 
        'Bishop': "♝",'King': "♚",'Queen': "♛"
    }

    _LETTERS = string.ascii_lowercase   # this is basically'abcdefghijklmnopqrstuvwxyz'

    def __init__(self, board_type="default", show=False):
        self.b_len = None
        if board_type in self.TYPES:
            self.b_len = self.TYPES[board_type]
        elif board_type.startswith("custom"):
            try:
                self.b_len = int(board_type.replace('custom', '').strip())
            except ValueError:
                pass

        if self.b_len is None:
            raise ValueError(f"Cannot create board for type '{board_type}'")
        if self.b_len < 1 or self.b_len > 26:
            raise ValueError(f"The board size has to be between 1 and 26, was {self.b_len}")

        self.board = self.board = [['___' for _ in range(self.b_len)]
                                   for _ in range(self.b_len)]
        if show:
            self.print_board()

The major changes are that board and b_len are now instance attributes instead of class attributes. Now you have to explicitly create an instance of the board, using board = Chessboard("default") or the like, before being able to use it. This will make all the if self.board != "UNITIALIZED": checks obsolote. Apart from that you could now have multiple boards to play with if you like. The __init__ constructor now more or less replaced new_board from the original implementation. As a bonus features I added an exception to signal the failure to create the board. This will also help you to detect the failure to do so programatically later on. You can read more about exceptions and exception handling in the documentation.
Next up on the list in this class is print_board. Same suggestion as above: no class method needed, make it an instance method. As mentioned above this lets you get rid of the initial check. One level of nesting gone. The method itself is not easily readable and left me thinking for a few moments. You define a helper function, printl as follows:
def printl():
    if len(str(cls.b_len)) == 2:
        print(' ', end='')
    for x in range(cls.b_len):
        print(' '*6 + f'{cls.letters[x]}', end='')
    print('\n')

After staring at it for a while I realized that this is the part where the "coordinate letters" are printed, and that you are trying to determine if you need an extra whitespace upfront to have it aligned correctly for boards with more than 10 fields. All of this can be done quite a bit shorter, for example:
def print_letters():
    print((" " if self.b_len >= 10 else "") 
          + "".join(f"{letter:>7}" for letter in self._LETTERS[:self.b_len]))

Let's break this down: (" " if n >= 10 else "") does add that extra leading whitespace for larger boards, f"{letter:>7}" prints letter right-aligned in a field of width 7 (6+the letter itself), and the letters are taken from self._LETTERS. All these formatted letters are then joined together and printed.
The rest of the code can more or less stay like this. If you are willing to print the numbers with a width of 2 for boards with a size smaller than 10, you can save yourself some code in the function above and in the line where the board is printed. I also advocate to avoid hardcoding additional leading and trailing newlines, since this takes control away from the user on how to have it printed to the console.
def print_board(self, leading=2, trailing=4):
    """Print the board to the console

    The number of leading and trailing newlines can be configured 
    and is 2, respectively 4 by default
    """
    def print_letters():
        print(" " + "".join(f"{letter:>7}" for letter in self._LETTERS[:self.b_len]))

    print("\n"*leading, end="")   # end="" is to avoid newlines for 0
    print_letters()
    for i in range(self.b_len):
        print(
            f'{self.b_len-i:>2}  {self.board[i]}  {self.b_len-i:>2}\n'
        )
    print_letters()
    print("\n"*trailing, end="")

If you want to prettify the print any further, you should have a look at the wide range of Unicode box characters.
The modifications to the other functions are mostly straightforward
def tile_convert(self, x, display_tile=False):
    if not display_tile:
        if isinstance(x, str):
            return self._LETTERS.index(x)
        else:
            return self._LETTERS[x]
    else:  # display_tile converts the letter in {letter}{number} to a number
        return self.b_len - int(x)

def l_num_to_coord(self, pos):
    return self.b_len - int(pos[1]), int(self.tile_convert(pos[0]))

def coord_to_tile(self, x, y):
    return f'{self.tile_convert(x)}{self.tile_convert(y, True)}'

def convert_color(self, color):
    if color == 'White':
        return 'b'
    if color == "Black":
        return 'w'

All of them would greatly benefit from a little bit of documentation, since their function is not always obvious from the name.

There is a lot left to discuss. I have not yet touched on the chess pieces or their interaction with the board. Maybe future me (Edit: seems like it did) or an other member of the community will further elaborate on that topic.
Until then: Happy coding!

1 Funny sidenote: I tested your program in a console with a black background. Under these circumstances the black pieces do actually look more like white pieces and vice versa.

