```table-of-contents
```
# Introducción 
Guigo Van Rossum 80 90, Primera versión 0.9 en el 91.
Características:
- Legibilidad: Usa identación (tabulares), facil para leer. 
- Miltiplataforma: MS- GNU/Linux - MACOS
- Tipado dinámico: No necesita declara el tipo de variables, infiere a partir de los daos que se ingresen.
- Interpretado: Código se ejecuta línea por línea en tiempo real, permite desarrollo rápido y depuración de código. 
- Amplia biblioteca: Muchos módulos acorde se necesite (bases de datos, textos, datos, imágenes, etc).
- Comunidad activa: Amplia comunidad global. 
## Aplicaciones:
- Desarrollo web. 
- Ciencia de datos.
- IA y Machine Learnin
- Automatiación de tareas
- Desarrollo de videojuegos
## Detalles: 
- Es sensible a mayúsculas y minúsculas
- Para ejecutar varias instrucciones podemos usar ; 
  instrucción1; instrucción 2; ....; ... ; 
# Fundamentos
## Tipos de datos
- int= Enteros
- float= Flotantes decimales.
- string= Cadenas de texto.
- boleanos = true o false, por ejemplo: es_mayor_edad = True. 
## Variables
- declaración: contenedores para manipular datos. 
  a = 25
  nombre = "Marcelo"
  fecha1= 22-12-1988
  estatura = 1.74
  es_estudiante = True
- Se pueden asignar el mismo valor a variables:
  a = b = c = d = 10
  Aquí, las variables a, b, c y d tendrán el valor de 10.
### Normas para nombrar variables:
- Las variables pueden tener: Mayúsculas, minúsculas y guión bajo ("nombre_variable").
- no pueden iniciar con un número
- No se puede usar palabras reservadas de python como else, if, for, while, etc. 
- Python distingue entre mayúsculas y minúsculas: Variable1 y variable1, son dos variables. 
 - Se recomienda usar nombres descriptivos par las variables. 
 Ejemplos de varaiables:
 suma_total
 nombre
 n1
 n2
 ____contador___
 Ejemplos de nombres no válidos:
 suma-total, usa -
 1n, empieza con número
 if, palabra reservada
## Operadores 
### Ariméticos
- Suma: +
- Resta: -
- Multiplicación: *  ()
- División: / 
- División entera: // (divide dos números y toma el entero resultante)
- Módulo: % (divide y toma el residuo o resto de la divisón 11%2=1 toma el 1)
- Exponenciación: **  (potenciación)
Ejemplos de operaciones:
```python
a = 5
b = 7
res_suma = a+b #suma 
res_resta = a-b #resta 
res_multiplicacion = a*b #multiplicacion
res_division = a/b #Division
res_div_entera =b//a #Dividion entera
res_modulo = a%b #Modulo 
res_potenciacion = a**b #potenciacion
print(f"si número 1 es {a} y el segundo es  {b} se dan estos resultados")
print(f"suma {res_suma}")
print(f"resta {res_resta}")
print(f"multiplicación  {res_multiplicacion}")
print(f"división  {res_division}")
print(f"división entera  {res_div_entera}")
print(f"división módulo {res_modulo}")
print(f"exponenciación o potenciación {res_potenciacion}")
```
 
### De comparación
Se comparan dos valores y devuelve un valor boleano (True o False)
Igualdad: == 
Diferente: !=
Mayor que: >
Menor que: <
Mayor o igual: >=
Menor o igual: <=
Ejemplos de operaciones:
```python
a = 5
b = 7 
res_igualdad = a == b
res_diferente = a != b
res_mayorque = a > b
res_menorque = a < b
res_mayor_igual = a >= b
res_menor_igual = a <= b
print(f"Si los números son {a} y {b} en las operaciones de compración las respuesas son:")
print(f"{a} igual que {b} = {res_igualdad}")
print(f"{a} diferente que {b} = {res_diferente}")
print(f"{a} mayor que {b} = {res_mayorque}")
print(f"{a} menor que {b} = {res_menorque}")
print(f"{a} mayor o igual que {b} = {res_mayor_igual}")
print(f"{a} menor o igual que {b} = {res_menor_igual}")
```
### Lógicos
and = devuelve True si ambas condiciones se cumplen
or  =  devuelve True si una de las condiciones se cumple
not =  invierte el valore del resultado si fue True devuelve False y viceversa. 
```python
a = 5
b = 7 
res_and = (a > 2) and (b < 5)  #True 
res_or = (a > 2) or (b < 5)  #True 
res_not = not (a > 2) #False 
print(f"Operación and: ({a} > {b}) and ({b} < 5) = {res_and}")
print(f"Operación or: ({a} > {b}) or ({b} < 5) = {res_or}")
print(f"Operación not: not ({a} > 2) = {res_not}" )
```
**Nota:** Python aplica orden de operaciones: 
1. Paréntesis
2. Potenciación
3. Multiplicación/división, 
4. Suma/Resta
5. Operadores de comparación
6. Operadores lógicos
# Estructuras condicionales
## IF 
Ejecuta un bloque de código si una condición se cumple.
Sintaxis:
if condicion:  
  
   # Bloque de código a ejecutar si la condición es verdadera  
   instrucciones
Aquí dos ejemplos:
```python
#Ejemplo1
a = 5
b = 7
if (a < b):
	print(f"El número {a} es menor que {b} y por eso ve este mensaje")
	
#Ejemplo2
edad = 18
if edad >= 18:
	print(f"La edad es {edad}, por ello es mayor de edad")
```
## if else
Ejecuta un bloque de código en caso que la condición se cumpla y otro bloque de código en el caso contrario, es decir, cuando no se cumple. 
Sintaxis:
if condición:
	Bloque de código cuando se cumple la condición
else:
	Bloque de código cuando no se cumple la condición
```python
edad = 15
if edad >= 18:
	print("Es mayor de edad")
else:
	print("Es menor de edad")
	
```
## if-elif-else
Permite ejecutar múltiples condiciones y múltiples bloques de código 
Sintaxis:
if condición1:
	bloque de código 1 a ejecutar si la condicion1 es verdadera 
elif condicion2:
	bloque de código 2 a ejecutar si la condicion2 es verdadera 
elif codición3:
	bloque de código 3 a ejecutar si la condicion2 es verdadera 
else:
	bloque de código 4 a ejecutar como caso contrario final ya que no se cumplen ninguna de las condiciones previas. 
Ejemplo:
```python
calificacion = 5
"""
Considere esta tabla
calificación = 10; excelente
calificación < 10 and calificacion >=8 = muy buena
calificación < 8 and calificacion >=6 = buena
calificación < 6 and calificacion >=4 = regular
Caso contrario = insuficiente
"""
if calificacion == 10:
	print(f"Calificación es {calificacion}, por ello es, excelente")
elif calificacion < 10 and calificacion >=8:
	print(f"Calificación es {calificacion}, por ello es, muy buena")
elif calificacion < 8 and calificacion >=6:
	print(f"Calificación es {calificacion}, por ello es, buena")
elif calificacion < 6 and calificacion >=4:
	print(f"Calificación es {calificacion}, por ello es, regular")
else:
	print(f"Calificación es {calificacion}, por ello es, insuficiente")
```
# Blucles / loops
Repiten bloques de código n veces o hasta que se cumplan condiciones determinadas.
## For
Itera sobre una secuencia (lista, tuplas o cadena) o cualquier objeto iterable. 
Sintaxis. 
For _variable_ in _secuencia_: 
	bloque de código a repetir
Ejemplo 1:
```python
frutas = ["plátano", "manzana", "naranja"]
for fruta in frutas:
	print(f"\nAquí la fruta: {fruta} que hay en el vector frutas")
"""
Aquí la fruta: plátano que hay en el vector frutas
- Aquí la fruta: manzana que hay en el vector frutas
- Aquí la fruta: naranja que hay en el vector frutas
"""
```
Ejemplo 2
```python
print("Números del 1 al 5 que se multiplican por 2 con blucle for")
"""
Range es una lista virtual, no se toma en cuenta el úlitmo valor (6). range (valor inicial, valor final, incremento o paso).
"""
for numero in range (1,6): 
	res = numero * 2
	print(f"\nEl número es {numero} * 2 que da = {res}")
```
## While
Repeite un bloque de código mientras la condición establecida tenga el estado "True", es decir, se cumpla o sea verdadera. 
Sintaxis:
``` python
while condicion:
	Bloque de código a repetir
```
Ejemplo:
```python
contador = 0 
while contador <= 3:
	print(f"El contador tiene valor {contador}")
	contador = contador + 1  #mire aqui cómo se ha manejado el contador
"""
NOTESE QUE EL BLUCLE SE REPITE 4 VECES YA QUE INICIA CON CERO (0).
- El contador tiene valor 0
- El contador tiene valor 1
- El contador tiene valor 2
- El contador tiene valor 3
"""
```
Ejemplo 2. 
```python
contador = 0 
while contador <= 3:
	print(f"El contador tiene valor {contador}")
	contador += 1 #mire aqui cómo se ha manejado el contador
"""
NOTESE QUE EL BLUCLE SE REPITE 4 VECES YA QUE INICIA CON CERO (0).
- El contador tiene valor 0
- El contador tiene valor 1
- El contador tiene valor 2
- El contador tiene valor 3
"""
```
## Control de blucles
Son instrucciones especiales para controlar el flujo dentro de un blucle. 
### Break
Sale prematuramente del bucle, esto quiere decir indepedientemente de la condición de finalización.  El tener un break el flujo del bucle se detiene y avanza a la siguiente línea de código o instrucción fuera del bucle. 
Ejemplo 1
```python
contador = 0
while True:
	print(f"Número {contador}")
	contador += 1
	if contador == 3: 
		break
print("Ha salido del bucle con break")
```
**NOTA:** Fíjese que while no tiene una condición por lo que sería una condición indefinida, sin embargo, dentro del bucle está el "if" del cual se evalúa una condición (contador = 3) y se aplica "break".
_¿Qué pasaría si no existiera esta línea "contador += 1"?_
### Continue
Permite saltar las líneas de código que sigan luego de la instrucción en la iteración. 
Ejemplo 1:
```python
for i in range(10):
	if i%2 == 0:
		print(f"\nEste número {i} es par, luego se usa continue continue")
		continue
		print("Este mensaje nunca se verá por el 'continue' ")
	print(f"Esta es la siguiente línea de código, ya que el número {i} no es par")
```
### Pass
Esta es una operación nula, Se usa como marcador de posición cuando se necesita una instrucción sintácticamente, pero no se desea hacer nada cuando eso suceda. 
Ejemplo: 
```python
for i in range(10):
	pass
print("Antes, en el bloque for se uso 'pass', el blucle termino y no se ha hecho nada")
```
# Listas
Son estructuras de datos, permiten organizarlos y almacenarlos de manera eficiente. En python existen listas, tuplas, diccionarios y conjuntos. 
## Listas 
Es una estrcutura de datos mutable (que puede cambiar) y ordenada, las listas pueden almacenar diversos tipos de datos y se deben encerrar en corchetes, separados por comas(,).
Sintaxis:
```python
nombre_lista=[elemento1,elemento2,elemento3]
```
Ejemplo:
```python
frutas = ["manzana", "naranja", "fresa"]
```
Para aceder a los elementos de una lista, se debe emplear el índice, considerando que empieza en la posición cero:
```python
frutas = ["manzana", "naranja", "fresa"]
print(frutas[0]) # manzana
print(frutas[1]) # naranja
print(frutas[2]) # fresa
```
En las listas también se puede acceder a ellas desde el final, empleando índices negativos, en este caso, temina en el -1. 
Ejemplo:
```python
frutas = ["manzana", "naranja", "fresa"]
print(frutas[-1]) #fresa úlitmo a la derecha
print(frutas[-2]) #naranja 
print(frutas[-3]) #manzana primero a la izquierda
```
### Metodos en las listas
La manipulación o modificación de las listas se lo hace considerando:
- append(elemento): _agrega_ un elemento al _final de la lista_.
- insert(indice.elemento): _inserta_ un elemento en una _posición específica de la lista_.
- remove(elemento): _elimina_ la _primera aparición de un elemento_ en la _lista_.
- pop(indice): _elimina y devuelve el elemento en una posición específica de la lista_.
- sort(): _Ordena_ los _elementos_ de la lista de _manera ascendente_.
- reverse():  _inveirte el orde_ de los _elementos_ de la lista.
ejemplo:
```python
frutas = ["manzana", "naranja", "fresa"]
#Append agregar elemento
frutas.append("sandía")
frutas.append("melon")
frutas.append("naranjilla")
print(frutas)
#insertar elemento
frutas.insert(2,"cacao")
print(frutas)
#Eliminar fruta
frutas.remove("fresa")
print(f"se ha eliminado fresa y quedan {frutas}")
#Pop indica
fruta_eliminada=frutas.pop(0)
print(f"se ha eliminado manzana quedan{frutas}")
print(f"fruta eliminada, {fruta_eliminada}")
#Orden ascendente
frutas.sort()
print(frutas)
#reverse
frutas.reverse()
print(frutas)
```
### Listas de comprensión
Es una forma consistente de crear nuevas listas a partir de secuencias existentes. Permiten filtrar y transformar los elementos de las listas en una sola línea de código.  Son _ordenadas y mutables_.
Sintaxis
```python
	nueva_lista=[expresion for elemento in secuencia if condicion]
```
Ejemplo
```python
numeros=[1,2,3,4,5]
cuadrados = [x ** 2 for x in numeros if x%2 ==0 ]
print(cuadrados) #Se imprimen 4 y 16 ya que 2**2= 4 y 4**2=16
```
Aquí, se crea una nueva lista llamada cuadrados, que contiene los cuadrados de los números pares de la lista numeros. La expresión x ** 2 eleva cada elemento al cuadrado, y la condición if x % 2 == 0 filtra solo los números pares.
## Tuplas
Son estructuras de datos o elementos _inmutables y ordenados_. Se encierran en paréntesis () separados por comas (,).
### Creación y acceso
Aquí se crea una lista:
punto = (1,2,3,4,5)
Para acceder a los elementos se usa el indice entre corchetes, similar a las listas. 
print[lista(indice_numero)]
Ejemplo
```python
punto = (1,2,3,4,5)
print(punto[0]) #imprime el número 1
print(punto[4]) #imprime el número 5
```
**NOTA:** Es necesario considerar en todo momento que las tuplas son inmutables.  Se pueden usar con colecciones de datos que no deben ser modificados (coordenadas o configuraciones).
### Métodos de tuplas
Si bien las tuplas son inmutables, existen métodos útiles para trabajar con ellas. 
- count(elemento): devuelve el número de veces que se repite el elemento en una tupla. 
- index(elemento): devuelve el indice de la primera aparción de un elemento en la tupla. Opcionalmente se puede especificar el incio y fin de la búsqueda. 
- len(tupla): Si bien no es propio de la tupla, esta función devuelve el tamaño de la tupla. 
Ejemplo:
```python
mi_tupla = (1,2,3,4,5,2,3,1)
print(mi_tupla.index(1)) #devuelve 0
print(mi_tupla.index(5)) #devuelve 4
print(mi_tupla.index(3, 1, 7)) #devuelve 2
```
## Diccionarios
Es una estrucutra de datos _mutable y no ordenada_ permite almacenar datos clave-valor, por lo que en el diccionario cada elemento tiene su respectiva clave y varlor. 
Sintaxis: 
```python
diccionario = {clave1:valor1, clave2:valor2, clave3:valor3}
```
Si la clave o valor son string se deben poner con comillas ("").
Ejemplo: 
```python
datos={"nombre":"Marcelo","apellido":"Sotaminga","edad":36,"estado civil":"Casado"}
print(datos["nombre"]) #Marcelo 
print(datos["apellido"]) #Sotaminga
print(datos["edad"]) #36
print(datos["estado civil"]) #Casado 
```
También se puede usar el método get. 
Ejemplo 
```python
datos={"nombre":"Marcelo","apellido":"Sotaminga","edad":36,"estado civil":"Casado"}
print(datos.get("nombre")) #Marcelo 
print(datos.get("apellido")) #Sotaminga
print(datos.get("edad")) #36
print(datos.get("estado civil")) #Casado 
print(datos.get("celular")) #None
```
### Métodos de diccionarios
Los diccionarios tienen varios métodos para acceder a los elementos y manipularos, entre ellos:
- keys(): devuelve una vista de todas las claves del diccionario. 
- values(): devuelve una vista de todos los valores del diccionario. 
- items(): devuelve una vista de todos los pares clave-valor del diccionario. 
- update(otro_diccionario): actualiza el diccionario con los pares clave-valor de otro diccionario. 
```python
datos={"nombre":"Marcelo","apellido":"Sotaminga","edad":36,"estado civil":"Casado"}
print(datos.keys())
print(datos.values())
print(datos.items())
datos.update({"tipo sangre":"O+"})
print(datos)
```
## Conjuntos
Es una estructura de datos _mutable y no ordenada_ permite almacenar un _colección de datos única_ se cierran entre llaves {} o se crean con la función set().
Sintaxis:
```python
nombre_conjunto = {elemento1, elemento2, elemento3}
nombre_conjunto2 = set([elemento1, elemento2, elemento3])
```
Operaciones:
```python
operacion_union = nombre_conjunto | nombre_conjunto2
operacion_interseccion = nombre_conjunto & nombre_conjunto2
operacion_diferencia = nombre_conjunto - nombre_conjunto2
operacion_diferencia_simetrica = nombre_conjunto ^ nombre_conjunto2
```
Ejemplo: 
```python
frutas={"naranja", "pera", "manzana", "platano",1}
numeros={1,2,3,4,5}
union= frutas | numeros
print(union)
interseccion = frutas & numeros
print(interseccion)
diferencia = frutas - numeros
print(diferencia)
diferencia_simetrica = frutas ^numeros
print(diferencia_simetrica)
```
### Métodos de conjuntos
En la manipulación de elementos en conjuntos, algunos métodos comunes son:
- add(elemento): agrega un elemento al conjunto. 
- remove(elemento): elimina el elemento del conjunto, sino existe da un error. 
- discard(elemento): elimina el elemento del conjunto -si está presente-, sino existe, no hace nada.
- clear(): elimina todos los elementos del conjunto.
Ejemplos
 ```python
frutas={"naranja", "pera", "manzana", "platano",1}
frutas.add("naranjilla")
print(frutas)
frutas.remove("pera")
print(frutas)
frutas.discard("naranja")
print(frutas)
frutas.clear()
print(frutas)
```
# Test 1
```python
def multiplicar (a, b):  
	return a * b
  
resultado = multiplicar (5, 3) + multiplicar (2, 4)
print(resultado)
```
# Funciones
Son bloques de código que encapsulan tareas específicas para que puedan ser ejecutadas cuando se las necesite. 
Permiten tener un código organizado, evitar repetirlo y crear programas modulares y comprensibles. 
## Definición y llamada de funciones
Se usa una palabra clave para nombrarlas (identificarlas) y unos paréntesis (), se debe identar luego de los dos puntos.  Opcionalmente, se puede especificar los parámetros dentro de los paréntesis. 
 ```python
def saludo ():
	print("Hola mundo")
saludo()
```
## Parámetros y argumentos
Los parámetros son valores que se insertan en la función al momento de llamarla, estos parámetros se ubican en los paréntesis. 
Ejemplo:
```python
def saludo(nombre):
	print(f"Hola soy {nombre}")
saludo("Marcelo Sotaminga")
saludo("Mónica")
```
## Valores de retorno
Las funciones pueden devolver el valor que sale de ella, se lo hace con la palabra clave _return_ 
```python
def suma(a, b):
	return a + b
	
resultado= suma (5,6)
print(resultado)
```
## Funciones anónimas (lambda)
Son funciones sin nombres, definidas en una sola línea.  Por lo general se usan en cosas muy pequeñas.
```python
cuadrado_numero = lambda x: x**2
print(cuadrado_numero(5))
```
## Alcance de las variables (globales vs locales)
Las _variables definidas dentro de una función son locales_, es decir, accesibles solo por la función.  Por otro lado, las variables definidas fuera de una función son globales y pueden ser llamadas (accesibles) denro de cualquier parte del programa. 
```python
def funcion():
	variable_local= 18
	print(f"La variable local es {variable_local}")
variable_global=20
def funcion2():
	print(f"La variable_global es {variable_global}")
funcion()
funcion2()
print(variable_local)
print (variable_global)
```
Ejemplo 2:
```python
def funcion():
    variable_local = 10
    print(variable_local)  # Accesible dentro de la función
variable_global = 20
def funcion2():
    print(variable_global)  # Accesible desde cualquier lugar
funcion()  # Imprime 10
funcion2()  # Imprime 20
print(variable_global)  # Imprime 20
print(variable_local)  # Genera un error, la variable no está definida en este alcance.
```
Ejemplo3:
 ```python
def calcular_media(*numeros):
	suma=sum(numeros)
	cantidad=len(numeros)
	media=suma/cantidad
	return media,suma,cantidad
	
suma, media, cantidad = calcular_media(1,2,3,4,5)
print(f"La media de {suma} sobre {cantidad}, es: {media}")
```
### Funciones definidas por el usuario
```python
def sumar_2 (x):
	return x+3
sumar= lambda x:x+3
print("sumarle al número...", sumar(5))
```
#### Documentación de funciones (docstring)
Una buena práctica es documentar las funciones docstrings, estos -docstrings- son cadenas de texto que describen el propósito, alcance, detalles de los parámetros y el valor de retorno de una función. Se colocan inmediatamente después de la definición de la función y se encierran en triples comillas dobles. 
```python
def area_rectangulo(base, altura):
	"""
	Aqui se calcula el área de un rectángulo, ingresando los valores de base y altura. 
	Args: 
		base (float): Base del rectángulo
		Altura (float): Altura del rectángulo
	Return: 
		(float): área del rectángulo.
	"""
	area= base * altura
	print(f"el areaa es {area}")
	return base,altura,area
area_rectangulo(4,5)
```
### Funciones con número de argumentos variables
En python se puede agregar variación en el número de argumentos usando el operador asterisco (\*) antes de nombe del parámetro en la declaración de la función.
Ejemplo:
```python
def suma_variable (*numeros):
	total = 0
	for numero in numeros:
		total += numero
	return total
print(suma_variable(1,2,3,4,5))
print(suma_variable(1,2,3))
```
	
**NOTA:** Podemos definir las funciones que necesitemos a fin de modulizar el código; estas funciones puedes ser reutilizadas. 
# Manejo de errores y excepciones
Python cuenta con mecanismos para manejas los errores, para ello, se utiliza de manera controlada el uso de las excepciones. Esto permite capturar y manejar errores específicos sin que el programa se detenga abruptamente. 
## Errores comunes en Python
### Error de sintaxis (SyntaxError)
Ocurre cuando el código no sigue las reglas de sintaxis de Python, por ejemplo: olvidar los dos puntos después de la declaración de la función. 
```python
def mi_función (a,b) #Aquí faltan los :
	print("hola")
```
### Error de nombre (ErrorName)
Aparece cuando se hace referencia a una variable o función que ha sido definido. 
```python
print(variable_no_definida)
```
### Error de tipo (TypeError)
Ocurre cuando se realiza una operación con un tipo de datos incompatibles. Por ejemplo, intentar sumar una cadena string. 
```python
suma = 5 + "10"
```
### Error de indice (IndexError)
Ocurre cuando se intenta acceder a un índice fuera del rango válido de una lista o sencuencia
```python
lista = [1,2,3,4,5]
print(lista[10])
```
## Manejo de excepciones
Pemrite manejar los errores de manera controlada con declaraciones try, except y opcionalmente finally. 
### try
El bloque try contiene el código que puede generar una excepción, si ocurre una excepción en el bloque try el flujo se transfiere bloque except que le corresponda. 
```python
try:
	#Código que puede generar una excepción
	resultado = 10 / 0 #División por cero
	print(resultado)
except ZeroDivisionError:
	print("Error: División por cero")
```
### except
El bloque except especifica el tipo de excepción que se desea capturar y manejar. Puedes tener múltiples bloques except para manejar diferentes tipos de excepciones.
```python
try:
	#Código que generea una excepción
	resultado = 10/0 #Division por cero
	print(resultado)
except ZeroDivisionError:
	print("Error no se puede dividir para cero")
except ValueError:
	print("Error, valor inválido")
```
### Finally
El bloque finally es opcional y se ejecuta siempre, independientemente de si ocurrió una excepción o no. Se utiliza comúnmente para realizar tareas de limpieza o liberación de recursos.
```python
try:
	#Código que puede generar una excepción
	archivo = open("archivo.txt", "r")
	 # Realizar operaciones con el archivo
except FileNotFoundError:
	print("Error archivo no encontrado")
finally:
	archivo.close()# Cerrar el archivo siempre, incluso si ocurre una excepción
```
## Excepciones personalizadas
Python permite crear excepciones personalizadas acorde lo necesite el programa que se esté codificando. 
Para crear una excepción personalizada, debes crear una clase que herede de la clase base Exception o de una de sus subclases.
```python
def funcion():
	#Códio que puede crear una excepcion personalizada
	if condicion:
		raise Exception("Descripción del error")
 
try:
	funcion()
except Exception as e:
	print(f"error {str(e)}")
```
En este ejemplo, se define una función llamada funcion(). Dentro de la función, se verifica una condición y, si se cumple, se genera una excepción utilizando la declaración raise. En lugar de crear una clase personalizada, se utiliza directamente la clase base Exception para generar la excepción.
Luego, se utiliza un bloque try-except para capturar y manejar la excepción. La variable e se utiliza para acceder a la descripción del error proporcionada al generar la excepción.
El manejo de errores y excepciones es una parte fundamental de la programación en Python. Te permite manejar situaciones inesperadas de manera controlada y evitar que tu programa se bloquee o se detenga abruptamente.
Cuando ocurre un error en tu código, Python genera una excepción. Al utilizar bloques try-except, puedes capturar y manejar estas excepciones de manera adecuada. Puedes especificar diferentes bloques except para manejar distintos tipos de excepciones y realizar acciones específicas en cada caso.
Además, el bloque finally te permite ejecutar código de limpieza o liberación de recursos, independientemente de si ocurrió una excepción o no. Esto es útil para garantizar que ciertas acciones se realicen siempre, como cerrar archivos o conexiones de base de datos.
# Entradas y salidas
La entrada y salida de datos permite que el usuario manipule los archivos. Se puede solicitar información al usuario, mostrar resultados en pantalla, leer o escribir datos en archivos externos. 
## Entrada de datos del usuario
```python
"""
Aquí se van a solicitar el nombre y edad al usuario y luego se imprimen estas variables.
"""
nombre = input("ingresa tu nombre:")
edad = input("Ingresa la edad:")
print("Hola," + nombre + "!")
print("Tienes," + edad + "años.")
```

**NOTA:** La función input() siempre devuelve una cadena de texto. Si deseas trabajar con otros tipos de datos, como números enteros o flotantes, debes realizar una conversión explícita utilizando funciones como int() o float().
```python
edad = int(input("Ingresa tu edad: "))  
  
if edad >= 18:  
    print("Eres mayor de edad.")  
else:  
    print("Eres menor de edad.")
```
En este ejemplo, se solicita al usuario que ingrese su edad y se convierte el valor ingresado a un número entero utilizando int(). Luego, se utiliza una estructura condicional para verificar si la edad es mayor o igual a 18 y mostrar un mensaje correspondiente.
### Salida de datos
Para mostrar informacion en internet usamos la función print(), se pueden usar uno o más argumentos y los muestra en consola. 
Se puede usar f-string (formateo de cadenas) para incrustar variables directamente dentro de una cadena de texto. 
```python
nombre = "Marcelo"
edad = 36
print(f"Hola soy {nombre} y tengo {edad} años")
```
En este caso, las variables se incrustan dentro de la cadena utilizando llaves {} y se precede la cadena con la letra f para indicar que es una f-string.
## Lectura de archivos
Para leer un archivo, primero se lo debe abrir usando la función open() en mode de lectura. ("r"); luego se puede leer el contenido del archivo con método como read() o readlines().
```python
"""
En este ejemplo, se abre el archivo "datos.txt" en modo de lectura utilizando open(). Luego, se lee todo el contenido del archivo utilizando el método read() y se almacena en la variable contenido. Finalmente, se muestra el contenido en la pantalla y se cierra el archivo utilizando el método close().
"""
archivo = open("archivo.txt", "r")
contenido = archivo.read()
print(contenido)
archivo.close()
```
## Escritura de archivos
Para escribir en un archivo, lo abrimos en modo de escritura ("w"), usando la función open(), si el archivo no existe, se creará si ya existe el contenido del archivo se sobreescribirá
```python
"""
En este ejemplo, se abre el archivo "datos.txt" en modo de escritura utilizando open(). Luego, se escribe la cadena "Hola, esto se ha escrito" en el archivo utilizando el método write(). Finalmente, se cierra el archivo utilizando el método close().
"""
archivo = open("archivo.txt","w" )
archivo.write("Hola, esto se ha escrito")
archivo.close()
```
**NOTA:** Es importante cerrar siempre los archivos después de utilizarlos para liberar los recursos del sistema. 
Otra manera de abrir archivos es con la _declaración_ **with**, esta abre y cierra automáticamente los archivos una vez que se ha salido de la declaración.
Ejemplo:
```python
"""
el archivo se abre utilizando la declaración with y se cierra automáticamente una vez que se sale del bloque with, incluso si ocurre una excepción.
"""
with open("archivo.txt", "w") as archivo
	archivo.write("Hola, esto se ha escrito")
	print(archivo)
```
# Importación y creación de módulos
Un módulo es un archivo que contiene la definición de funciones, clases, variables que se pueden usar en otros programas. Esta importación permite la reutilización de código de manera eficiente y personalizada. 
**Nota:** Python viene con una amplia biblioteca estándar de módulos que proporcionan funcionalidades adicionales. Estos módulos están disponibles sin necesidad de instalarlos por separado.
## Importar módulos
Para usar un módulo se debe importar con la declaración import, se lo puede importar completamente o un módulo del archivo. 
```python
"""
se importa el módulo math utilizando la declaración import. Luego, se utiliza la función sqrt() del módulo math para calcular la raíz cuadrada de 25.
"""
import math
resultado = math.sqrt(25)
print(resultado)
```
También podemos importar funciones específicas de un módulo utilizando la sintaxis from módulo import función.
```python
"""
	se importa solo la función sqrt() del módulo math, lo que nos permite utilizarla directamente sin tener que precederla con el nombre del módulo.
	
"""
from math import sqrt
resultado = sqrt(25)
print(f"El resultado es {resultado}")
```
## Funciones y clases de módulos estandar
La biblioteca estándar de python tiene muchas bibliotecas, módulos con funciones y clases, algunos ejemplos:
- Math: Proporciona funciones matemáticas, como sqrt() (raíz cuadrada), sin() (seno), cos() (coseno), entre otras.
- Random: Ofrece funciones para generar números aleatorios, como random() (número aleatorio entre 0 y 1), randint() (número entero aleatorio en un rango), entre otras.
- Datetime: Permite trabajar con fechas y horas, como datetime.now() (fecha y hora actual), datetime.date() (fecha), datetime.time() (hora), entre otras.
```python
"""
	Proposito: Ejemplo de uso de funciones estandar random y datetime. 
	
	Args: N/A
	
	Return: N/A
	
"""
import random
import datetime
numero_aleatorio = random.randint(1,20)
print(f"Aquí se muestra un número aleatorio entre 1 y 20: {numero_aleatorio}")
fecha_actual = datetime.datetime.now()
print(f"La fecha actual es: {fecha_actual}")
```
**NOTA:** para conocer los detalles de las funciones y sus argumentos, es recomendable que se consulte la documentación oficial en todos los casos.
## Crear y usar módulos personalizados
Se debe crear un archivo de python, definir las funciones, clases y variables que se desee incluir en el módulo. 
Ejemplo 1. Se creará un archivo "mi_modulo.py"
```python
"""
	Proposito:
	
	Args: 
	
	Return: 
	
"""
#Mi_módulo.py
def saludar (nombre)
	print(f"Hola!, {nombre}")
def calcular_suma (n1,n2)
	return a+b
	
```
Luego, podemos importar y utilizar las funciones definidas en mi_modulo.py en otro archivo Python.
```python
"""
	Proposito:
	
	Args: 
	
	Return: 
	
"""
import mi_modulo
mi_modulo.saludar("juan")
resultado = mi_modulo.calular_suma(5,3)
print(resultado)
```
## Organización del código en módulos
En el caso de proyectos de gran tamaño, es recomendable el usao de módulos separados según su funcionalidad.  Esto nos permite conservar un código más legible, agrupado en módulos y fácil de mantener.
Por ejemplo, podemos tener un módulo operaciones.py que contenga funciones relacionadas con operaciones matemáticas, y otro módulo utilidades.py que contenga funciones de uso general.
```python
"""
	Proposito:
	
	Args: 
	
	Return: 
	
"""
#Operaciones.py
def suma(a,b):
	return (a,b)
def restar(a,b):
	return (a,b)
#utilidades.py
def imprimir_mensaje(mensaje):
	print(mensaje)
	
def obtener_mensaje_usuario():
	return input ("ingresa tu nombre: ")
```
Luego, podemos importar y utilizar estas funciones en nuestro programa principal.
```python
"""
	Proposito:
	
	Args: 
	
	Return: 
	
"""
import operaciones
import utilidades
resultado = operaciones.sumar(5, 3)
utilidades.imprimir_mensaje(f"El resultado de la suma es: {resultado}")
nombre = utilidades.obtener_nombre_usuario()
utilidades.imprimir_mensaje(f"Hola, {nombre}!")
```
Al organizar nuestro código en módulos, podemos reutilizar funciones y mantener un código más estructurado y agrupado en módulos.
## Paquetes
Un paquete es una forma de organizar módulos relacionados en una estructura jerárquica de directorios. Los paquetes permiten organizar los módulos relacionados y evitar conflictos entre módulos. 
### Crear y utilizar paquetes
Para crear un paquete, dentro de un directorio se debe crear un archivo especial llamado \_\_init\_\_.py Este archivo puede estar vacío o contener código de inicialización de paquetes. 
Por ejemplo, creamos un directorio llamado mi_paquete con la siguiente estructura:
```python
"""
	Proposito:
	
	Args: 
	
	Return: 
	
"""
mi_paquete/  
    __init__.py  
    modulo1.py  
    modulo2.py
```
Luego, podemos importar y utilizar los módulos del paquete en nuestro programa.
```python
"""
	Proposito: En este ejemplo, se importan los módulos modulo1 y modulo2 del paquete mi_paquete y se utilizan las funciones definidas en ellos.
	
	Args: 
	
	Return: 
	
"""
from mi_paquete import modulo1, modulo2
modulo1.funcion1()
modulo2.funcion2()
```
```python
"""
	Proposito:
	
	Args: 
	
	Return: 
	
"""
x = 5
y = "3"
z = x + int(y)
print(z)
```